From 7c7b4e79bfa92478efc75d5bcc4cd7ae520f5e07 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:44:33 +0000 Subject: [PATCH 1/4] docs: sync from base-std@be6d045 --- .../issue-rwa/announce-a-distribution.mdx | 102 ++++++++++-- .../issue-rwa/apply-a-multiplier.mdx | 152 ++++++++++++++---- .../issue-rwa/cancel-blocked-units.mdx | 2 +- .../issue-rwa/create-an-asset-token.mdx | 4 + .../issue-rwa/pause-transfers.mdx | 8 +- .../issue-your-stablecoin.mdx | 15 +- .../issue-stablecoins/pause-activity.mdx | 15 +- .../restrict-who-can-hold.mdx | 18 ++- .../create-composite-policy.mdx | 83 +++++++--- .../finalize-update-admin.mdx | 43 +++-- .../max-composite-child-policies.mdx | 13 +- .../min-composite-child-policies.mdx | 21 +-- .../pending-policy-admin.mdx | 38 +++-- .../i-policy-registry/policy-admin.mdx | 31 ++-- .../i-policy-registry/renounce-admin.mdx | 35 ++-- .../i-policy-registry/stage-update-admin.mdx | 43 +++-- .../i-policy-registry/update-allowlist.mdx | 54 +++++-- .../i-policy-registry/update-blocklist.mdx | 48 ++++-- .../i-policy-registry/update-composite.mdx | 45 +++--- .../interfaces/ib20-asset/announce.mdx | 78 ++++++--- .../interfaces/ib20-asset/batch-mint.mdx | 62 ++++--- .../interfaces/ib20-asset/effective-at.mdx | 27 +++- .../interfaces/ib20-asset/multiplier.mdx | 27 ++-- .../interfaces/ib20-asset/operator-role.mdx | 22 +-- .../ib20-asset/scaled-balance-of.mdx | 41 ++++- .../ib20-asset/to-scaled-balance.mdx | 45 +++++- .../interfaces/ib20-asset/to-ui-amount.mdx | 27 +++- .../interfaces/ib20-asset/ui-multiplier.mdx | 28 +++- .../ib20-asset/update-multiplier.mdx | 45 ++++-- .../interfaces/ib20-asset/wad-precision.mdx | 16 +- .../interfaces/ib20-factory/create-b20.mdx | 80 +++++---- .../ib20-factory/is-b20-initialized.mdx | 35 ++-- .../interfaces/ib20-factory/is-b20.mdx | 30 ++-- .../reference/interfaces/ib20/grant-role.mdx | 24 +-- .../reference/interfaces/ib20/is-paused.mdx | 38 +++-- .../b20/reference/interfaces/ib20/pause.mdx | 30 +++- .../interfaces/ib20/paused-features.mdx | 24 ++- .../reference/interfaces/ib20/policy-id.mdx | 30 ++-- .../interfaces/ib20/renounce-role.mdx | 14 +- .../reference/interfaces/ib20/revoke-role.mdx | 14 +- .../interfaces/ib20/seize-holder-policy.mdx | 28 +++- .../interfaces/ib20/seize-receiver-policy.mdx | 32 ++-- .../reference/interfaces/ib20/seize-role.mdx | 28 ++-- .../interfaces/ib20/seize-with-memo.mdx | 81 +++++++--- .../interfaces/ib20/set-role-admin.mdx | 30 ++-- .../b20/reference/interfaces/ib20/unpause.mdx | 26 ++- .../interfaces/ib20/update-policy.mdx | 41 +++-- 47 files changed, 1267 insertions(+), 506 deletions(-) diff --git a/docs/build-on-base/issue-rwa/announce-a-distribution.mdx b/docs/build-on-base/issue-rwa/announce-a-distribution.mdx index e194c57ee..cfbd031c3 100644 --- a/docs/build-on-base/issue-rwa/announce-a-distribution.mdx +++ b/docs/build-on-base/issue-rwa/announce-a-distribution.mdx @@ -1,13 +1,13 @@ --- -title: "Announce a Distribution" -description: "Publish a B20 Asset announcement and distribute additional units to holders in the same transaction." -keywords: ["announce distribution", "B20 announcement", "asset distribution", "batchMint distribution"] +title: "Announce a Corporate Action" +description: "Wrap a holder-impacting action with an onchain disclosure using the B20 Asset announce function." +keywords: ["announce corporate action", "B20 announcement", "asset distribution", "batchMint", "stock split", "treasury burn"] --- import { AssetDemo } from "/snippets/AssetDemo.jsx" import RwaDisclaimer from "/snippets/RwaDisclaimer.mdx" -Bracket a holder-impacting action with an onchain description and supporting URI. This example announces a five-percent stock dividend and issues additional shares with `batchMint` in the same transaction. +Bracket a holder-impacting action with an onchain description and supporting URI. `announce` emits `Announcement` before the inner calls and `EndAnnouncement` after, giving indexers a reliable bracket for every corporate action. @@ -20,10 +20,28 @@ The demo uses a local browser-generated account to submit real transactions on * -New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@v1.0.0`, `viem@2.55.11`, and Base Foundry `v1.1.1`. +New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. -## Announce and Distribute the Stock Dividend +## How `announce` works + +`announce` takes `internalCalls`, a single-use `id`, a `description`, and an optional `uri`. + +- **`internalCalls`** — ABI-encoded calldata blobs targeting this asset. Each blob must be at least 4 bytes. Pass an empty array for a notice with no on-chain effect. +- **`id`** — chosen by the caller; consumed on success. Reuse reverts `AnnouncementIdAlreadyUsed`. After success, `isAnnouncementIdUsed(id)` returns `true`. +- **`description` / `uri`** — operator-supplied strings. The asset does not verify them. + +Execution order: + +1. Emit `Announcement(caller, id, description, uri)`. +2. Run the inner calls atomically. +3. Emit `EndAnnouncement(id)`. + +If any inner call fails, the whole transaction reverts and `id` is not consumed. Nesting `announce` inside an inner call reverts `AnnouncementInProgress`. + +The caller must hold `OPERATOR_ROLE`. Any other caller reverts `AccessControlUnauthorizedAccount`. Inner calls keep their own role gates — if an inner call needs `MINT_ROLE` or `BURN_ROLE`, grant those to the operator as well. + +## Scenario 1 — Stock dividend with additional shares {/* sample: stock-dividend-ts */} @@ -66,16 +84,76 @@ export async function announceStockDividend(token: Address, holders: Address[]) ``` -See the [B20 token standard](/build-on-base/issue-rwa/create-an-asset-token) for the complete interface, roles, and policies. + +This scenario issues additional shares. A reinvested dividend that only rescales the UI belongs in Scenario 2. + + +The operator needs `OPERATOR_ROLE` and `MINT_ROLE`. Recipients must pass `MINT_RECEIVER_POLICY`. `MINT` must not be paused. On success the asset emits `Announcement`, then one `Transfer(address(0), recipient, amount)` per recipient, then `EndAnnouncement`. + +## Scenario 2 — Stock split or reverse split + +Wrap `updateUIMultiplier(newMultiplier, effectiveAt)`. A 2-for-1 split uses `2e18`; a reverse split uses a value below `1e18`. The operator needs `OPERATOR_ROLE` only. + +```solidity Solidity lines wrap expandable + function announceSplit(address token, uint256 newMultiplier, uint256 effectiveAt) public { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeCall(IB20Asset.updateUIMultiplier, (newMultiplier, effectiveAt)); + IB20Asset(token).announce( + calls, + "2026-split-01", + "2-for-1 stock split", + "https://example.com/corporate-actions/2026-split-01" + ); + } +``` + +On success the asset emits `Announcement`, `UIMultiplierUpdated`, then `EndAnnouncement`. `UIMultiplierUpdated` means the schedule was recorded, not that the multiplier is already active. + +To replace a live pending update, cancel first then reschedule in one `announce`: + +```solidity Solidity lines wrap expandable + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(IB20Asset.cancelUIMultiplierUpdate, ()); + calls[1] = abi.encodeCall(IB20Asset.updateUIMultiplier, (newMultiplier, effectiveAt)); + IB20Asset(token).announce(calls, id, description, uri); +``` + +## Scenario 3 — Treasury burn + +Wrap `burnWithMemo(amount, memo)`. The call burns the operator's own balance. The operator needs `OPERATOR_ROLE` and `BURN_ROLE`. `BURN` must not be paused. + +```solidity Solidity lines wrap expandable + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeCall(IB20Asset.burnWithMemo, (amount, memo)); + IB20Asset(token).announce(calls, id, "Treasury burn", uri); +``` + +On success the asset emits `Announcement`, `Transfer(operator, address(0), amount)`, `Memo`, then `EndAnnouncement`. `totalSupply` decreases. + +## Scenario 4 — Notice with no on-chain effect + +Pass an empty `internalCalls` array. The operator needs `OPERATOR_ROLE` only. The asset emits `Announcement` then `EndAnnouncement` with nothing between them. The `id` is still consumed. + +```solidity Solidity Title="Notice only" + IB20Asset(token).announce(new bytes[](0), id, description, uri); +``` + +## Common errors + +| Error | Cause | Fix | +| --- | --- | --- | +| `AccessControlUnauthorizedAccount(caller, OPERATOR_ROLE)` | Caller does not hold `OPERATOR_ROLE`. | Grant `OPERATOR_ROLE` to the operator. | +| `AnnouncementIdAlreadyUsed(id)` | A prior successful `announce` consumed `id`. | Choose a new single-use `id`. | +| `InternalCallMalformed(call)` | An inner-call blob is shorter than 4 bytes. | Pass ABI-encoded calldata that includes a selector. | +| `AnnouncementInProgress()` | An inner call targeted `announce`. | Do not nest `announce`. | +| `InternalCallFailed(call)` | An inner call reverted. The reason is not bubbled. | Replay `call` directly to see the underlying error, then fix the cause. | + +Typical inner causes of `InternalCallFailed`: missing `MINT_ROLE` or `BURN_ROLE`, paused `MINT` or `BURN`, `UIMultiplierUpdateExists`, `PolicyForbids`, `SupplyCapExceeded`, `InsufficientBalance`. A Solidity `Panic` (for example overflow) propagates raw and is not wrapped as `InternalCallFailed`. -The unique announcement ID is marked used and the batch mint executes between `Announcement` and `EndAnnouncement`. +On success, the unique announcement `id` is marked used and inner events sit between `Announcement` and `EndAnnouncement`. - -This example issues additional shares. It does not distribute a cash dividend. - - ## See Also diff --git a/docs/build-on-base/issue-rwa/apply-a-multiplier.mdx b/docs/build-on-base/issue-rwa/apply-a-multiplier.mdx index ebaa2d113..dc6fc0eb5 100644 --- a/docs/build-on-base/issue-rwa/apply-a-multiplier.mdx +++ b/docs/build-on-base/issue-rwa/apply-a-multiplier.mdx @@ -1,16 +1,20 @@ --- -title: "Apply a Multiplier" -description: "Apply a B20 Asset multiplier to update displayed unit balances without migrating holders. A stock split is one example." -keywords: ["asset token multiplier", "B20 multiplier", "scaled token balance", "stock split"] +title: "Schedule a Stock Split" +description: "Schedule a B20 Asset multiplier update so displayed share counts change at a future time without rewriting raw balances." +keywords: ["asset token multiplier", "B20 multiplier", "scaled token balance", "stock split", "updateUIMultiplier"] --- import { AssetDemo } from "/snippets/AssetDemo.jsx" import RwaDisclaimer from "/snippets/RwaDisclaimer.mdx" -Run a 2-for-1 stock split by setting the B20 Asset multiplier to `2e18`. Raw balances stay unchanged, while `scaledBalanceOf` and conversion helpers expose the post-split share amounts. +Schedule a 2-for-1 stock split by calling `updateUIMultiplier(2e18, effectiveAt)` on a B20 Asset. Raw `balanceOf`, `totalSupply`, and transfer amounts stay unchanged. Wallets and indexers read the post-split share count through `balanceOfUI` and related views after `effectiveAt` passes. + +The routine path is `updateUIMultiplier`, not the deprecated `updateMultiplier`. `updateMultiplier` applies a value immediately and clears any pending update — use it only as an emergency override. + + ## Demo @@ -23,49 +27,139 @@ The demo uses a local browser-generated account to submit real transactions on * New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@v1.0.0`, `viem@2.55.11`, and Base Foundry `v1.1.1`. -## Apply and Verify the Split Multiplier +## How the Multiplier Works + +Raw `balanceOf` is unchanged by a split. The multiplier changes only the derived UI view: + +| Read | Formula | +| --- | --- | +| `uiMultiplier()` | Effective multiplier at `block.timestamp` | +| `balanceOfUI(account)` | `balanceOf(account) * uiMultiplier() / 1e18` | +| `totalSupplyUI()` | `totalSupply() * uiMultiplier() / 1e18` | +| `toUIAmount(raw)` / `fromUIAmount(ui)` | Convert at the effective multiplier | + +The multiplier is an 18-decimal WAD: `1e18` is `1.0`. A 2-for-1 split uses `2e18`. A 1-for-2 reverse split uses `5e17`. Integer division rounds down, so a round trip through `toUIAmount` and `fromUIAmount` can lose up to one unit in the last place when `multiplier != 1e18`. Prefer 18 decimals for equities to keep that effect small. + +## Schedule and Verify the Split Multiplier + +### Prerequisites + +- A B20 Asset you administer. +- `DEFAULT_ADMIN_ROLE` to grant `OPERATOR_ROLE`. +- A future `effectiveAt` timestamp and a `newMultiplier` in `(0, MAX_UI_MULTIPLIER]`. + +Only an account holding `OPERATOR_ROLE` may call `updateUIMultiplier`, `cancelUIMultiplierUpdate`, or `updateMultiplier`. Any other caller reverts `AccessControlUnauthorizedAccount`. + +### Grant `OPERATOR_ROLE` and schedule -{/* sample: stock-split-ts */} -```typescript TypeScript lines wrap expandable highlight={7} +```typescript TypeScript lines wrap expandable highlight={7,8} import { type Address } from "viem"; import { publicClient } from "../../shared/clients.js"; import { assetAbi } from "../abi.js"; import { sendContract } from "../write.js"; -export async function runTwoForOneSplit(token: Address, holder: Address) { - await sendContract({ address: token, abi: assetAbi, functionName: "updateMultiplier", args: [2n * 10n ** 18n] }); - const multiplier = await publicClient.readContract({ address: token, abi: assetAbi, functionName: "multiplier" }); - const scaled = await publicClient.readContract({ address: token, abi: assetAbi, functionName: "scaledBalanceOf", args: [holder] }); - if (multiplier !== 2n * 10n ** 18n) throw new Error("Multiplier was not updated"); - return scaled; +export async function scheduleTwoForOneSplit(token: Address, operator: Address) { + await sendContract({ address: token, abi: assetAbi, functionName: "grantRole", + args: [await publicClient.readContract({ address: token, abi: assetAbi, functionName: "OPERATOR_ROLE" }), operator] }); + const effectiveAt = BigInt(Math.floor(Date.now() / 1000) + 86400); // 1 day from now + await sendContract({ address: token, abi: assetAbi, functionName: "updateUIMultiplier", + args: [2n * 10n ** 18n, effectiveAt] }); + const newMultiplier = await publicClient.readContract({ address: token, abi: assetAbi, functionName: "newUIMultiplier" }); + if (newMultiplier !== 2n * 10n ** 18n) throw new Error("Split not scheduled"); + return effectiveAt; } ``` -{/* sample: stock-split-solidity */} -```solidity Solidity lines wrap expandable highlight={2} - function splitStock(address token, address holder) public returns (uint256 scaledBalance) { - IB20Asset(token).updateMultiplier(2e18); - require(IB20Asset(token).multiplier() == 2e18, "multiplier not updated"); - scaledBalance = IB20Asset(token).scaledBalanceOf(holder); - } +```solidity Solidity lines wrap expandable highlight={3,4} +function scheduleAndVerifySplit(address token, address operator) public returns (uint256 scheduled) { + IB20Asset(token).grantRole(IB20Asset(token).OPERATOR_ROLE(), operator); + uint256 effectiveAt = block.timestamp + 1 days; + IB20Asset(token).updateUIMultiplier(2e18, effectiveAt); + // While pending: uiMultiplier() is still 1e18, newUIMultiplier() is 2e18 + require(IB20Asset(token).newUIMultiplier() == 2e18, "split not scheduled"); + return effectiveAt; +} ``` -{/* sample: stock-split-cli */} -```bash CLI lines wrap expandable highlight={1} -base-cast send "$TOKEN_ADDRESS" "updateMultiplier(uint256)" 2000000000000000000 \ +```bash CLI lines wrap expandable highlight={2} +base-cast send "$TOKEN_ADDRESS" "grantRole(bytes32,address)" "$OPERATOR_ROLE" "$OPERATOR" \ --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" -base-cast call "$TOKEN_ADDRESS" "scaledBalanceOf(address)(uint256)" "$HOLDER" --rpc-url "$RPC_URL" +EFFECTIVE_AT=$(( $(date +%s) + 86400 )) +base-cast send "$TOKEN_ADDRESS" "updateUIMultiplier(uint256,uint256)" 2000000000000000000 "$EFFECTIVE_AT" \ + --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" +base-cast call "$TOKEN_ADDRESS" "newUIMultiplier()(uint256)" --rpc-url "$RPC_URL" ``` -See the [B20 token standard](/build-on-base/issue-rwa/create-an-asset-token) for the complete interface, roles, and policies. +On success, the asset emits `UIMultiplierUpdated(oldMultiplier, newMultiplier, effectiveAtTimestamp)`. That event fires when the update is **recorded**, not when the multiplier becomes active. + +### Read the live pending state + +```solidity Solidity +asset.uiMultiplier(); // still the old (current) multiplier +asset.newUIMultiplier(); // scheduled target +asset.effectiveAt(); // flip timestamp +``` + +A second `updateUIMultiplier` while a pending update is live reverts `UIMultiplierUpdateExists`. To replace a pending update, cancel first, then reschedule. + +### Confirm after `effectiveAt` + +When `block.timestamp >= effectiveAt`, `uiMultiplier()` returns the new multiplier. Maturation does **not** emit an event and does **not** write storage. Do not wait for a second event at the flip. -`multiplier()` returns `2e18`; raw balances remain unchanged while scaled balances double. +`uiMultiplier()` returns `2e18` at or after `effectiveAt`. Raw `balanceOf` stays unchanged. `balanceOfUI` doubles. - -This guide targets Beryl `base-std@v1.0.0`. Integrations must distinguish raw ERC-20 units from scaled display balances. - +## Cancel a Pending Update + +Call before `effectiveAt` to discard the pending split: + +```solidity Solidity +asset.cancelUIMultiplierUpdate(); +``` + +Emits `UIMultiplierUpdateCancelled(cancelledMultiplier, cancelledEffectiveAt)`. Calling with no live pending (including after maturity) reverts `UIMultiplierUpdateDoesNotExist`. + +To cancel and reschedule atomically, wrap both calls in `announce`: + +```solidity Solidity lines wrap +bytes[] memory calls = new bytes[](2); +calls[0] = abi.encodeCall(IB20Asset.cancelUIMultiplierUpdate, ()); +calls[1] = abi.encodeCall(IB20Asset.updateUIMultiplier, (secondMultiplier, secondEffectiveAt)); +asset.announce(calls, "reorder-2026-Q3", "reorder split", "https://disclosures.example/"); +``` + +## Emergency Override + +Use `updateMultiplier(newMultiplier)` only when a pending update is wrong and you cannot wait for `effectiveAt`. It applies the value immediately and clears any pending update. + +| Situation | Events (in order) | +| --- | --- | +| Live pending (`effectiveAt > block.timestamp`) | `UIMultiplierUpdateCancelled`, then `MultiplierUpdated(new)`, then `UIMultiplierUpdated(old, new, block.timestamp)` | +| Matured or no pending | `MultiplierUpdated(new)`, then `UIMultiplierUpdated(old, new, block.timestamp)` | + +`MultiplierUpdated` is deprecated. Process only `UIMultiplierUpdated` to avoid handling the same update twice. + +## Integrator Rules + +- Listen for `UIMultiplierUpdated`, not the deprecated `MultiplierUpdated`. +- If `effectiveAtTimestamp > block.timestamp`, treat the update as pending until that time. +- Maturation emits nothing. Do not wait for a second event at the flip. +- On `UIMultiplierUpdateCancelled`, discard the pending update. +- Detect a live pending with `effectiveAt() > block.timestamp`. Do **not** check `effectiveAt() == 0`. + +## Common Errors + +| Error | Cause | Fix | +| --- | --- | --- | +| `AccessControlUnauthorizedAccount(caller, OPERATOR_ROLE)` | Caller lacks `OPERATOR_ROLE`. | Grant `OPERATOR_ROLE` to the operator. | +| `InvalidMultiplier()` | `newMultiplier` is zero or above `MAX_UI_MULTIPLIER`. | Pass a value in `(0, MAX_UI_MULTIPLIER]`. | +| `EffectiveAtInPast(effectiveAt)` | `effectiveAt <= block.timestamp`. | Pass a strictly future timestamp. | +| `EffectiveAtTooFar(effectiveAt)` | `effectiveAt > type(uint64).max`. | Pass a timestamp that fits in `uint64`. | +| `UIMultiplierUpdateExists(effectiveAt)` | A live pending update already exists. | Cancel first, or cancel-then-reschedule in one `announce`. | +| `UIMultiplierUpdateDoesNotExist()` | `cancelUIMultiplierUpdate` with no live pending (including after maturity). | Call only while `effectiveAt() > block.timestamp`. | +| `AnnouncementIdAlreadyUsed(id)` | `announce` reused an `id`. | Choose a new single-use `id`. | +| `InternalCallFailed(call)` | An inner call in `announce` reverted. | Fix the encoded calldata and retry. | ## See Also diff --git a/docs/build-on-base/issue-rwa/cancel-blocked-units.mdx b/docs/build-on-base/issue-rwa/cancel-blocked-units.mdx index 8ac651ea1..59a66880b 100644 --- a/docs/build-on-base/issue-rwa/cancel-blocked-units.mdx +++ b/docs/build-on-base/issue-rwa/cancel-blocked-units.mdx @@ -60,7 +60,7 @@ The holder balance and total supply each fall by 100 EXM. -`burnBlocked` destroys shares; it does not move them to the issuer. Block the holder under `TRANSFER_SENDER_POLICY` first. +`burnBlocked` destroys shares and reduces `totalSupply`; it does not move them to another account. If you need to move a holder's balance to a safekeeping account without changing `totalSupply`, use `seizeWithMemo` instead. Block the holder under `TRANSFER_SENDER_POLICY` before calling `burnBlocked`. ## See Also diff --git a/docs/build-on-base/issue-rwa/create-an-asset-token.mdx b/docs/build-on-base/issue-rwa/create-an-asset-token.mdx index 7f605f403..7f6e07c86 100644 --- a/docs/build-on-base/issue-rwa/create-an-asset-token.mdx +++ b/docs/build-on-base/issue-rwa/create-an-asset-token.mdx @@ -25,6 +25,8 @@ New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asse ## Create and Verify the Stock Token +Choose the **Asset** variant when you need configurable decimals, announcements, a scheduled UI multiplier, extra metadata, or `batchMint`. Asset decimals must be in the range `[6, 18]`; values outside that range revert `InvalidDecimals`. The type is sealed into the token address at creation and cannot change. + {/* sample: stock-create-ts */} ```typescript TypeScript lines wrap expandable highlight={15} @@ -71,6 +73,8 @@ export async function createStockToken() { ``` +The factory assigns a deterministic address from `(variant, sender, salt)`. Address byte `[10]` is `0x00` for Asset tokens. Reusing the same `(variant, sender, salt)` triple reverts `TokenAlreadyExists`. Optional `initCalls` — such as the role grants above — run on the new token in the same transaction; the factory drops access after `createB20` returns. + See the [B20 token standard](/build-on-base/issue-rwa/create-an-asset-token) for the complete interface, roles, and policies. diff --git a/docs/build-on-base/issue-rwa/pause-transfers.mdx b/docs/build-on-base/issue-rwa/pause-transfers.mdx index 7b1b29b5d..b71372102 100644 --- a/docs/build-on-base/issue-rwa/pause-transfers.mdx +++ b/docs/build-on-base/issue-rwa/pause-transfers.mdx @@ -49,14 +49,18 @@ base-cast send "$TOKEN_ADDRESS" "unpause(uint8[])" "[0]" \ ``` -See the [B20 token standard](/build-on-base/issue-rwa/create-an-asset-token) for the complete interface, roles, and policies. +`pause` and `unpause` each accept a `PausableFeature[]` array. Pass `[0]` to target `TRANSFER` only. The four pausable features are `TRANSFER` (0), `MINT` (1), `BURN` (2), and `SEIZE` (3). Pausing one feature does not affect the others — a holder can still transfer while `MINT` is paused, and a minter can still mint while `TRANSFER` is paused. + +Passing an empty array reverts `EmptyFeatureSet`. A feature already in the requested state is a no-op; the call does not revert. After the call, read `isPaused(feature)` to confirm the current state. The event emitted (`Paused` or `Unpaused`) carries the exact array you passed, not the resulting paused set. + +If a caller attempts an operation while its feature is paused, the call reverts `ContractPaused(feature)`. Transfer pause state changes without pausing mint or burn. -Pause and unpause use separate roles. Grant recovery authority more narrowly than emergency pause authority. +`pause` requires `PAUSE_ROLE`. `unpause` requires `UNPAUSE_ROLE`. These are separate roles — grant recovery authority more narrowly than emergency pause authority. ## See Also diff --git a/docs/build-on-base/issue-stablecoins/issue-your-stablecoin.mdx b/docs/build-on-base/issue-stablecoins/issue-your-stablecoin.mdx index 751c9851a..b775ee12c 100644 --- a/docs/build-on-base/issue-stablecoins/issue-your-stablecoin.mdx +++ b/docs/build-on-base/issue-stablecoins/issue-your-stablecoin.mdx @@ -6,7 +6,7 @@ description: "Create a fiat-backed stablecoin on Base with one B20 factory call. import { StablecoinDemo } from "/snippets/StablecoinDemo.jsx" -Create a fiat-backed token with one call to the [B20 Factory](/specifications/b20/specification-overview#factory), using the `STABLECOIN` variant. Decimals are fixed at 6, and the token carries an immutable ISO currency code. +Create a fiat-backed token with one call to the [B20 Factory](/specifications/b20/specification-overview#factory), using the `STABLECOIN` variant. Decimals are fixed at `6`, and the token carries an immutable ISO currency code. The type is sealed into the token address at creation and cannot change. ## Demo @@ -20,6 +20,17 @@ The demo uses a local browser-generated account to submit real transactions on * New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@v1.0.0`, `viem@2.55.11`, and Base Foundry `v1.1.1`. +## How the Stablecoin Variant Works + +Passing `STABLECOIN` to `createB20` writes discriminant byte `0x01` into position `[10]` of the token address. After the call returns: + +- `decimals()` returns `6` (hardcoded; you do not pass a decimals value). +- `currency()` returns the immutable ISO code you provided (for example `"USD"`). +- The `IB20Stablecoin` surface is live at that address. +- Asset-only selectors (`announce`, `updateUIMultiplier`, `batchMint`) do not execute on this address. + +The `currency` field is required and must be uppercase ASCII `A`–`Z` only. An empty code reverts `MissingRequiredField`; any other byte reverts `InvalidCurrency`. + ## Create and Verify the Stablecoin @@ -84,6 +95,8 @@ export async function createStablecoin() { ``` +The `params` blob is ABI-encoded with a leading `version` byte (currently `1`) as `B20StablecoinCreateParams`: `version`, `name`, `symbol`, `initialAdmin`, and `currency`. Optional `initCalls` run on the new token in the same transaction; the factory drops access after they complete. + See the [B20 token standard](/specifications/b20/specification-overview) for the complete interface, roles, and policies. diff --git a/docs/build-on-base/issue-stablecoins/pause-activity.mdx b/docs/build-on-base/issue-stablecoins/pause-activity.mdx index a1eea085a..c6dc25790 100644 --- a/docs/build-on-base/issue-stablecoins/pause-activity.mdx +++ b/docs/build-on-base/issue-stablecoins/pause-activity.mdx @@ -8,6 +8,8 @@ import { StablecoinDemo } from "/snippets/StablecoinDemo.jsx" If something goes wrong, halt activity fast. B20 pausing is **granular**: stop transfers, mints, or burns independently. `PAUSE_ROLE` and `UNPAUSE_ROLE` are separate, so the ability to stop the token can be held more widely than the ability to resume it. +The four pausable features are `TRANSFER`, `MINT`, `BURN`, and `SEIZE` (enum values `0`–`3`). Each is independent — pausing `MINT` and `BURN` leaves `TRANSFER` active. A caller who holds `MINT_ROLE` still cannot mint while `MINT` is paused. + ## Demo @@ -58,9 +60,20 @@ See the [B20 token standard](/specifications/b20/specification-overview) for the -Transfer is enum value `0`. The function accepts an array; never pass a hash such as `keccak256("TRANSFER")`. +`pause` and `unpause` accept a `PausableFeature[]` array. Passing an empty array reverts `EmptyFeatureSet`. Transfer is enum value `0`; never pass a hash such as `keccak256("TRANSFER")`. A feature already in the requested state is a no-op — the call does not revert. +## Pausable Features + +| Feature | Enum value | Gates | +| --- | --- | --- | +| `TRANSFER` | `0` | `transfer`, `transferFrom`, and memo variants | +| `MINT` | `1` | `mint`, `mintWithMemo`, `batchMint` | +| `BURN` | `2` | `burn`, `burnWithMemo`, `burnBlocked` | +| `SEIZE` | `3` | `seizeWithMemo` | + +When a paused feature blocks a call, the transaction reverts `ContractPaused(feature)`. The error names only the one feature that blocked the call. + ## See Also diff --git a/docs/build-on-base/issue-stablecoins/restrict-who-can-hold.mdx b/docs/build-on-base/issue-stablecoins/restrict-who-can-hold.mdx index f59035f15..73fec1db0 100644 --- a/docs/build-on-base/issue-stablecoins/restrict-who-can-hold.mdx +++ b/docs/build-on-base/issue-stablecoins/restrict-who-can-hold.mdx @@ -20,6 +20,14 @@ The demo uses a local browser-generated account to submit real transactions on * New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@v1.0.0`, `viem@2.55.11`, and Base Foundry `v1.1.1`. +## How Policies Work + +The Policy Registry is a singleton precompile that stores each member list once. A token stores only a `uint64` policy ID per scope. When a gated function runs, the token calls `isAuthorized(policyId, account)` on the registry. Many tokens can share one policy; an update to the policy is immediately visible to every token that references it. + +Scopes gate specific functions. `TRANSFER_SENDER_POLICY` and `TRANSFER_RECEIVER_POLICY` check the sender and recipient on every `transfer` and `transferFrom`. `MINT_RECEIVER_POLICY` checks the recipient on every `mint`. All three default to `ALWAYS_ALLOW` (`0`) until you bind a policy. + +An **allowlist** authorizes only accounts in the set. An empty allowlist authorizes nobody — seed your intended holders before binding the policy. + ## Create and Bind a Holder Allowlist @@ -73,16 +81,20 @@ done ``` -See the [B20 token standard](/specifications/b20/specification-overview) for the complete interface, roles, and policies. - The token returns the new policy ID for both transfer policy scopes. -An allowlist denies every account not in the policy. Seed intended holders before binding it, and retain the `PolicyCreated` ID. +An allowlist denies every account not in the policy. Seed intended holders before binding it, and retain the `PolicyCreated` ID. Membership batches are capped at 64 accounts per call; call `updateAllowlist` to add more after creation. +## Update Membership + +After creation, only the policy admin can add or remove accounts. Call `updateAllowlist(policyId, true, accounts)` to add and `updateAllowlist(policyId, false, accounts)` to remove. The change is visible to every token that references the policy on the next call — no second `updatePolicy` is needed on the token. + +To combine a KYC allowlist with a sanctions blocklist, create an `INTERSECT` composite policy referencing both simple policies, then bind the composite ID to the token's scopes. See the [B20 token standard](/specifications/b20/specification-overview) for the full policy type reference. + ## See Also diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/create-composite-policy.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/create-composite-policy.mdx index d9a145786..4496642fc 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/create-composite-policy.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/create-composite-policy.mdx @@ -1,10 +1,8 @@ --- title: "IPolicyRegistry.createCompositePolicy" -description: "Generated B20 reference for createCompositePolicy(address,uint8,uint64[])." +description: "Creates a new composite policy that combines existing simple policies under a UNION or INTERSECT logic gate." --- - - ## Signature ```solidity IPolicyRegistry.sol @@ -16,35 +14,70 @@ function createCompositePolicy(address admin, PolicyType policyType, uint64[] ca | Selector | `0x6fdd1491` | | Canonical signature | `createCompositePolicy(address,uint8,uint64[])` | +## Parameters + +| Name | Type | Description | +|---|---|---| +| `admin` | `address` | Initial admin authorized to update child policies and transfer or renounce administration. Cannot be `address(0)`. | +| `policyType` | `PolicyType` (`uint8`) | Must be `UNION` or `INTERSECT`. | +| `childPolicyIds` | `uint64[]` | IDs of existing simple policies to combine. Count must be in `[MIN_COMPOSITE_CHILD_POLICIES, MAX_COMPOSITE_CHILD_POLICIES]` (2–4). | + +## Returns + +| Name | Type | Description | +|---|---|---| +| `newPolicyId` | `uint64` | The newly assigned composite policy ID. | + ## Description -Creates a new composite policy that combines existing simple policies under a logic -gate. -Dev: Child policies must be simple policies (ALLOWLIST or BLOCKLIST), never another composite. -The child-policy set is capped at 4. -Dev: Reverts with `IncompatiblePolicyType` when `policyType` is not UNION or INTERSECT. -Dev: Reverts with `ZeroAddress` when `admin` is `address(0)`. -Dev: Reverts with `ChildPoliciesOutsideOfRange` when `childPolicyIds.length` is not in -`[MIN_COMPOSITE_CHILD_POLICIES, MAX_COMPOSITE_CHILD_POLICIES]`. -Dev: Reverts with `PolicyNotFound` when any child policy does not exist. -Dev: Reverts with `InvalidChildPolicy` when any child policy is not a simple policy or a built-in policy. -Dev: Panics with arithmetic overflow (Panic 0x11) when the policy counter has reached its maximum value. -Param: admin Initial admin authorized to update child policies and transfer or renounce -administration. -Param: policyType UNION or INTERSECT. -Param: childPolicyIds Existing simple policy IDs to combine. -Return: newPolicyId The newly assigned composite policy ID. +Creates a composite policy that combines two to four existing simple policies (`ALLOWLIST` or `BLOCKLIST`) under a single logic gate: -## Access Control +| `policyType` | Authorized when | +|---|---| +| `UNION` | Any child authorizes the account | +| `INTERSECT` | Every child authorizes the account | + +The registry stores references to the children, not a snapshot of their members. Every call to `isAuthorized` reads each child's current member set, so updates to a child policy are immediately visible through the composite. -Permissionless creation, but state-changing registry calls require the feature to be active. +Creation is permissionless. The `admin` you supply is the only address that can later call `updateComposite`, `stageUpdateAdmin`, or `renounceAdmin` on this policy. -## Policy Interaction +On success, emits `PolicyCreated` and `PolicyAdminUpdated(newPolicyId, address(0), admin)`. -This is part of the singleton PolicyRegistry surface used by B20 policy scopes. +## Revert Conditions + +| Error | Condition | +|---|---| +| `ZeroAddress()` | `admin` is `address(0)` | +| `IncompatiblePolicyType()` | `policyType` is not `UNION` or `INTERSECT` | +| `ChildPoliciesOutsideOfRange()` | `childPolicyIds.length` is outside `[MIN_COMPOSITE_CHILD_POLICIES, MAX_COMPOSITE_CHILD_POLICIES]` (2–4) | +| `PolicyNotFound()` | Any child policy ID does not exist in the registry | +| `InvalidChildPolicy(childPolicyId)` | Any child is not an existing simple policy (composites and built-in sentinels are not valid children) | +| Panic `0x11` | The policy ID counter has reached its maximum value | + +## Access Control + +Permissionless — any caller may create a composite policy. ## Example -```solidity Usage Example -IPolicyRegistry(target).createCompositePolicy(arg0, arg1, arg2); +```solidity Title Create a KYC-and-sanctions composite +// 1. Create a KYC allowlist and a sanctions blocklist first. +uint64 kycId = registry.createPolicy(admin, PolicyType.ALLOWLIST); +uint64 sanctionsId = registry.createPolicy(admin, PolicyType.BLOCKLIST); + +// 2. Combine them: an account must be KYC'd AND not sanctioned. +uint64[] memory children = new uint64[](2); +children[0] = kycId; +children[1] = sanctionsId; + +uint64 gateId = registry.createCompositePolicy(admin, PolicyType.INTERSECT, children); + +// 3. Bind the composite to the token's transfer and mint scopes. +token.updatePolicy(PolicyScope.TRANSFER_SENDER_POLICY, gateId); +token.updatePolicy(PolicyScope.TRANSFER_RECEIVER_POLICY, gateId); +token.updatePolicy(PolicyScope.MINT_RECEIVER_POLICY, gateId); ``` + + +Updating a child policy's membership (via `updateAllowlist` or `updateBlocklist`) takes effect on the next `isAuthorized` call through any composite that references it. No second `updateComposite` is needed on the token. + diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/finalize-update-admin.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/finalize-update-admin.mdx index 5d8c71d98..3038dcce9 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/finalize-update-admin.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/finalize-update-admin.mdx @@ -1,10 +1,8 @@ --- title: "IPolicyRegistry.finalizeUpdateAdmin" -description: "Generated B20 reference for finalizeUpdateAdmin(uint64)." +description: "Completes a two-step admin transfer for a policy, promoting the pending admin to active admin." --- - - ## Signature ```solidity IPolicyRegistry.sol @@ -18,22 +16,43 @@ function finalizeUpdateAdmin(uint64 policyId) external; ## Description -Completes a two-step admin transfer. Promotes the caller to active admin and clears the pending slot. -Dev: Reverts with `PolicyNotFound` when `policyId` does not exist. -Dev: Reverts with `NoPendingAdmin` when no transfer is in flight. -Dev: Reverts with `Unauthorized` when the caller is not the staged pending admin. -Param: policyId Policy whose admin transfer is being finalized. +Completes a two-step admin transfer initiated by `stageUpdateAdmin`. The caller must be the address that was staged as the pending admin. On success, the caller becomes the active admin, the pending slot clears, and the previous admin loses the ability to update the policy. + +## Parameters + +| Name | Type | Description | +|---|---|---| +| `policyId` | `uint64` | The policy whose pending admin transfer is being finalized. | + +## Revert Conditions + +| Error | Condition | +|---|---| +| `PolicyNotFound()` | `policyId` does not exist in the registry. | +| `NoPendingAdmin()` | No admin transfer is staged for this policy. | +| `Unauthorized()` | The caller is not the staged pending admin. | ## Access Control -Callable only by the staged pending admin for the target policy; finalization promotes the caller to active admin. +Callable only by the staged pending admin for the target policy. Finalization atomically promotes the caller to active admin and clears the pending slot. The previous admin cannot update the policy after this call succeeds. + +## Behavior + +This function is the second step of the two-step admin transfer pattern: -## Policy Interaction +1. The current admin calls `stageUpdateAdmin(policyId, newAdmin)`, which sets the pending admin without transferring control. +2. The pending admin calls `finalizeUpdateAdmin(policyId)` to accept and complete the transfer. -This is part of the singleton PolicyRegistry surface used by B20 policy scopes. +Until `finalizeUpdateAdmin` is called, `policyAdmin(policyId)` still returns the current admin. After a successful call, `policyAdmin(policyId)` returns the new admin and `pendingPolicyAdmin(policyId)` is cleared. + +Emits `PolicyAdminUpdated(policyId, previousAdmin, newAdmin)`. ## Example ```solidity Usage Example -IPolicyRegistry(target).finalizeUpdateAdmin(arg0); +// Step 1: current admin stages the transfer +IPolicyRegistry(registry).stageUpdateAdmin(policyId, nextAdmin); + +// Step 2: nextAdmin finalizes +IPolicyRegistry(registry).finalizeUpdateAdmin(policyId); ``` diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/max-composite-child-policies.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/max-composite-child-policies.mdx index ac6f2e055..a3cd43b6b 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/max-composite-child-policies.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/max-composite-child-policies.mdx @@ -1,10 +1,8 @@ --- title: "IPolicyRegistry.MAX_COMPOSITE_CHILD_POLICIES" -description: "Generated B20 reference for MAX_COMPOSITE_CHILD_POLICIES()." +description: "Returns the maximum number of child policies a composite policy may reference." --- - - ## Signature ```solidity IPolicyRegistry.sol @@ -18,19 +16,18 @@ function MAX_COMPOSITE_CHILD_POLICIES() external view returns (uint256); ## Description -Maximum number of child policies a composite may reference, inclusive. Never reverts. -Return: Maximum permitted child-policy count. +Returns the upper bound on the number of child policies a composite policy may reference. The valid child count range is `[MIN_COMPOSITE_CHILD_POLICIES, MAX_COMPOSITE_CHILD_POLICIES]`, which is `2` through `4`. Never reverts. ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +Read-only. No role required. ## Policy Interaction -This is part of the singleton PolicyRegistry surface used by B20 policy scopes. +Used by `createCompositePolicy` and `updateComposite` to validate that the supplied child array length does not exceed this bound. Exceeding it reverts `ChildPoliciesOutsideOfRange`. ## Example ```solidity Usage Example -IPolicyRegistry(target).MAX_COMPOSITE_CHILD_POLICIES(); +uint256 max = IPolicyRegistry(target).MAX_COMPOSITE_CHILD_POLICIES(); ``` diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/min-composite-child-policies.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/min-composite-child-policies.mdx index 395d10e3b..60a18fbd8 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/min-composite-child-policies.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/min-composite-child-policies.mdx @@ -1,10 +1,8 @@ --- title: "IPolicyRegistry.MIN_COMPOSITE_CHILD_POLICIES" -description: "Generated B20 reference for MIN_COMPOSITE_CHILD_POLICIES()." +description: "Returns the minimum number of child policies a composite policy must reference." --- - - ## Signature ```solidity IPolicyRegistry.sol @@ -18,19 +16,22 @@ function MIN_COMPOSITE_CHILD_POLICIES() external view returns (uint256); ## Description -Minimum number of child policies a composite must reference, inclusive. Never reverts. -Return: Minimum permitted child-policy count. +Returns the minimum number of child policies a composite policy must reference, inclusive. The value is `2`. Never reverts. -## Access Control +A composite created with fewer than `MIN_COMPOSITE_CHILD_POLICIES` children reverts `ChildPoliciesOutsideOfRange`. The upper bound is enforced by `MAX_COMPOSITE_CHILD_POLICIES` (`4`). -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +## Returns -## Policy Interaction +| Name | Type | Description | +|---|---|---| +| _(unnamed)_ | `uint256` | Minimum permitted child-policy count (`2`). | + +## Access Control -This is part of the singleton PolicyRegistry surface used by B20 policy scopes. +Read-only. No role required. ## Example ```solidity Usage Example -IPolicyRegistry(target).MIN_COMPOSITE_CHILD_POLICIES(); +uint256 min = IPolicyRegistry(registry).MIN_COMPOSITE_CHILD_POLICIES(); // 2 ``` diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/pending-policy-admin.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/pending-policy-admin.mdx index abbf1cedf..1508dbd15 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/pending-policy-admin.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/pending-policy-admin.mdx @@ -1,10 +1,8 @@ --- title: "IPolicyRegistry.pendingPolicyAdmin" -description: "Generated B20 reference for pendingPolicyAdmin(uint64)." +description: "Returns the staged pending admin for a policy, or address(0) if no transfer is in flight." --- - - ## Signature ```solidity IPolicyRegistry.sol @@ -18,22 +16,32 @@ function pendingPolicyAdmin(uint64 policyId) external view returns (address); ## Description -Returns the currently-staged pending admin for `policyId`, or `address(0)` when -no transfer is in flight or for built-in sentinels, unknown IDs, and malformed IDs. -Never reverts. -Param: policyId Policy to query. -Return: Pending admin, or `address(0)`. +Returns the currently-staged pending admin for `policyId`, or `address(0)` when no transfer is in flight or for built-in sentinels, unknown IDs, and malformed IDs. Never reverts. + +The pending admin is set by `stageUpdateAdmin(policyId, newAdmin)` and cleared when `finalizeUpdateAdmin(policyId)` succeeds or when the current admin calls `stageUpdateAdmin` with `address(0)`. Until `finalizeUpdateAdmin` is called, `policyAdmin` still returns the current admin — the pending admin has no privileges yet. + +## Parameters + +| Name | Type | Description | +|---|---|---| +| `policyId` | `uint64` | Policy to query. | + +## Returns + +| Type | Description | +|---|---| +| `address` | Staged pending admin, or `address(0)` if none is set. | ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +Read-only. No role required. -## Policy Interaction +## Revert Conditions -This is part of the singleton PolicyRegistry surface used by B20 policy scopes. +Never reverts. -## Example +## Related -```solidity Usage Example -IPolicyRegistry(target).pendingPolicyAdmin(arg0); -``` +- `stageUpdateAdmin` — sets the pending admin. +- `finalizeUpdateAdmin` — promotes the pending admin to current admin; reverts `NoPendingAdmin` if nothing is staged, or `Unauthorized` if the caller is not the staged address. +- `policyAdmin` — returns the current (active) admin. diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/policy-admin.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/policy-admin.mdx index fbd514d0f..89c73ed95 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/policy-admin.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/policy-admin.mdx @@ -1,10 +1,8 @@ --- title: "IPolicyRegistry.policyAdmin" -description: "Generated B20 reference for policyAdmin(uint64)." +description: "Returns the current admin of a policy in the Policy Registry." --- - - ## Signature ```solidity IPolicyRegistry.sol @@ -16,23 +14,30 @@ function policyAdmin(uint64 policyId) external view returns (address); | Selector | `0x09dd0a47` | | Canonical signature | `policyAdmin(uint64)` | -## Description +## Parameters -Returns the current admin of `policyId`, or `address(0)` for built-in sentinels, -renounced policies, unknown IDs, and malformed IDs. Never reverts. -Param: policyId Policy to query. -Return: Current admin, or `address(0)`. +| Name | Type | Description | +|---|---|---| +| `policyId` | `uint64` | The policy to query. | -## Access Control +## Returns + +The current admin address, or `address(0)` for built-in sentinels (`ALWAYS_ALLOW`, `ALWAYS_BLOCK`), renounced policies, unknown IDs, and malformed IDs. -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +## Behavior -## Policy Interaction +Never reverts. Returns `address(0)` for any ID that has no admin — including sentinels and IDs that have never been created. + +After `renounceAdmin`, no address can be assigned as admin again. `policyAdmin` returns `address(0)` permanently for that policy. + +During a pending admin transfer, `policyAdmin` still returns the **current** admin. The staged nominee is readable via `pendingPolicyAdmin`. Administration transfers only when the nominee calls `finalizeUpdateAdmin`. + +## Access Control -This is part of the singleton PolicyRegistry surface used by B20 policy scopes. +View — no role required. ## Example ```solidity Usage Example -IPolicyRegistry(target).policyAdmin(arg0); +address admin = IPolicyRegistry(registry).policyAdmin(policyId); ``` diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/renounce-admin.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/renounce-admin.mdx index eeb2ce86e..f61f42e12 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/renounce-admin.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/renounce-admin.mdx @@ -1,10 +1,8 @@ --- title: "IPolicyRegistry.renounceAdmin" -description: "Generated B20 reference for renounceAdmin(uint64)." +description: "Permanently relinquishes administration of a policy, freezing its member set." --- - - ## Signature ```solidity IPolicyRegistry.sol @@ -18,22 +16,35 @@ function renounceAdmin(uint64 policyId) external; ## Description -Permanently relinquishes administration of `policyId`. The member set is frozen -and the policy can never be re-administered; `isAuthorized` queries continue to work. -Dev: Reverts with `PolicyNotFound` when `policyId` does not exist. -Dev: Reverts with `Unauthorized` when the caller is not the current admin. -Param: policyId Policy whose administration is being renounced. +Permanently relinquishes administration of `policyId`. After this call, the member set is frozen: no one can add or remove members, replace composite children, or assign a new admin. `isAuthorized` queries continue to work normally. + +Use this when you want to guarantee that a policy's membership can never change — for example, to make a compliance list immutable. + +## Parameters + +| Name | Type | Description | +|---|---|---| +| `policyId` | `uint64` | The policy whose administration is being renounced. | + +## Revert Conditions + +| Error | Condition | +|---|---| +| `PolicyNotFound()` | `policyId` does not exist in the registry. | +| `Unauthorized()` | The caller is not the current admin of `policyId`. | ## Access Control -Callable by the policy admin for the target policy. +Callable only by the current policy admin for `policyId`. -## Policy Interaction +## Behavior Notes -This is part of the singleton PolicyRegistry surface used by B20 policy scopes. +- Administration is gone permanently. There is no call that assigns a new admin after renounce. +- A staged pending admin (set via `stageUpdateAdmin`) is also cleared — `finalizeUpdateAdmin` cannot be used to recover the policy. +- Emits `PolicyAdminUpdated(policyId, previousAdmin, address(0))`. ## Example ```solidity Usage Example -IPolicyRegistry(target).renounceAdmin(arg0); +IPolicyRegistry(registryAddress).renounceAdmin(policyId); ``` diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/stage-update-admin.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/stage-update-admin.mdx index f97bec05c..d749edd6c 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/stage-update-admin.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/stage-update-admin.mdx @@ -1,10 +1,8 @@ --- title: "IPolicyRegistry.stageUpdateAdmin" -description: "Generated B20 reference for stageUpdateAdmin(uint64,address)." +description: "Stages a proposed new admin for a policy in the Policy Registry; the active admin does not change until the pending admin calls finalizeUpdateAdmin." --- - - ## Signature ```solidity IPolicyRegistry.sol @@ -16,25 +14,40 @@ function stageUpdateAdmin(uint64 policyId, address newAdmin) external; | Selector | `0x1d7ae695` | | Canonical signature | `stageUpdateAdmin(uint64,address)` | -## Description +## Parameters + +| Name | Type | Description | +|---|---|---| +| `policyId` | `uint64` | Policy whose admin is being staged. | +| `newAdmin` | `address` | Proposed new admin. Pass `address(0)` to clear a nomination that has not been finalized. | + +## Behavior + +Nominates `newAdmin` as the pending admin for `policyId`. The current admin does not change. `policyAdmin(policyId)` still returns the current admin. `pendingPolicyAdmin(policyId)` returns `newAdmin`. -Stages a proposed new admin for `policyId`. The active admin does not change -until `pendingAdmin` calls `finalizeUpdateAdmin`. -Dev: Reverts with `PolicyNotFound` when `policyId` does not exist. -Dev: Reverts with `Unauthorized` when the caller is not the current admin. -Param: policyId Policy whose admin is being staged. -Param: newAdmin Proposed new admin, or `address(0)` to clear any pending nomination. +The pending admin must call `finalizeUpdateAdmin(policyId)` to take effect. Until then, the current admin retains full update rights on the policy. + +Passing `address(0)` as `newAdmin` clears any previously staged nomination without transferring administration. + +Emits `PolicyAdminStaged(policyId, currentAdmin, pendingAdmin)`. ## Access Control -Callable by the policy admin for the target policy. +Callable only by the current policy admin for `policyId`. Any other caller reverts `Unauthorized`. -## Policy Interaction +## Revert Conditions -This is part of the singleton PolicyRegistry surface used by B20 policy scopes. +| Error | Condition | +|---|---| +| `PolicyNotFound()` | `policyId` does not exist in the registry. | +| `Unauthorized()` | Caller is not the current admin of `policyId`. | ## Example -```solidity Usage Example -IPolicyRegistry(target).stageUpdateAdmin(arg0, arg1); +```solidity Title Admin handoff +// Step 1 — current admin nominates a successor +IPolicyRegistry(registry).stageUpdateAdmin(policyId, nextAdmin); + +// Step 2 — nextAdmin finalizes (separate transaction, called by nextAdmin) +IPolicyRegistry(registry).finalizeUpdateAdmin(policyId); ``` diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/update-allowlist.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/update-allowlist.mdx index 44a37c27b..6681e0b95 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/update-allowlist.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/update-allowlist.mdx @@ -1,10 +1,8 @@ --- title: "IPolicyRegistry.updateAllowlist" -description: "Generated B20 reference for updateAllowlist(uint64,bool,address[])." +description: "Sets membership of a batch of accounts in an ALLOWLIST policy." --- - - ## Signature ```solidity IPolicyRegistry.sol @@ -18,25 +16,49 @@ function updateAllowlist(uint64 policyId, bool allowed, address[] calldata accou ## Description -Sets `accounts` membership in an ALLOWLIST policy to `allowed` in one batch. -Dev: Reverts with `PolicyNotFound` when `policyId` does not exist. -Dev: Reverts with `IncompatiblePolicyType` when the policy is not ALLOWLIST. -Dev: Reverts with `Unauthorized` when the caller is not the current admin. -Dev: Reverts with `BatchSizeTooLarge` when `accounts.length` exceeds the registry limit. -Param: policyId Policy to update. -Param: allowed Membership state to apply to every account in the batch. -Param: accounts Accounts to update. +Sets each address in `accounts` to `allowed` (add or remove) in an `ALLOWLIST` policy. The change takes effect on the next `isAuthorized` call against this policy. Every token and composite that references `policyId` sees the updated membership immediately — no second write on the token is needed. + +Membership batches are capped at 64 accounts (`MAX_BATCH_SIZE`). + +## Parameters + +| Name | Type | Description | +|---|---|---| +| `policyId` | `uint64` | The allowlist policy to update. | +| `allowed` | `bool` | `true` to add accounts to the set; `false` to remove them. | +| `accounts` | `address[]` | Accounts to update. Maximum 64 per call. | ## Access Control -Callable by the policy admin for the target policy. +Callable only by the current admin of `policyId`. Any other caller reverts `Unauthorized`. + +## Revert Conditions -## Policy Interaction +| Error | Condition | +|---|---| +| `Unauthorized()` | Caller is not the current policy admin. | +| `PolicyNotFound()` | `policyId` does not exist in the registry. | +| `IncompatiblePolicyType()` | The policy exists but is not an `ALLOWLIST`. | +| `BatchSizeTooLarge(maxBatchSize)` | `accounts.length` exceeds 64. | -This is part of the singleton PolicyRegistry surface used by B20 policy scopes. +## Events + +Emits `AllowlistUpdated(policyId, updater, allowed, accounts)` on success. ## Example -```solidity Usage Example -IPolicyRegistry(target).updateAllowlist(arg0, arg1, arg2); +```solidity Title Add accounts to an allowlist +IPolicyRegistry(registry).updateAllowlist(kycId, true, [alice, bob]); +``` + +After this call, `isAuthorized(kycId, alice)` and `isAuthorized(kycId, bob)` return `true`. Any token or composite policy already pointing at `kycId` reflects this without a separate `updatePolicy` call on the token. + +To remove an account, pass `false`: + +```solidity Title Remove an account from an allowlist +IPolicyRegistry(registry).updateAllowlist(kycId, false, [alice]); ``` + + +Only the current admin of a policy can call `updateAllowlist`. If you need a different party to manage membership, use `stageUpdateAdmin` and `finalizeUpdateAdmin` to transfer administration first. + diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/update-blocklist.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/update-blocklist.mdx index 40533d088..32070d6cd 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/update-blocklist.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/update-blocklist.mdx @@ -1,10 +1,8 @@ --- title: "IPolicyRegistry.updateBlocklist" -description: "Generated B20 reference for updateBlocklist(uint64,bool,address[])." +description: "Adds or removes accounts from a BLOCKLIST policy in a single batch call." --- - - ## Signature ```solidity IPolicyRegistry.sol @@ -16,27 +14,43 @@ function updateBlocklist(uint64 policyId, bool blocked, address[] calldata accou | Selector | `0x5c4e51b8` | | Canonical signature | `updateBlocklist(uint64,bool,address[])` | -## Description +## Parameters + +| Name | Type | Description | +|---|---|---| +| `policyId` | `uint64` | The BLOCKLIST policy to update. | +| `blocked` | `bool` | Membership state to apply to every account in the batch. `true` adds accounts to the blocklist; `false` removes them. | +| `accounts` | `address[]` | Accounts to update. Capped at 64 per call. | + +## Behavior + +Sets each address in `accounts` to the membership state `blocked` within the identified BLOCKLIST policy. A BLOCKLIST authorizes any account that is **not** in the set, so adding an account (`blocked = true`) causes `isAuthorized` to return `false` for that account, and removing it (`blocked = false`) restores authorization. -Sets `accounts` membership in a BLOCKLIST policy to `blocked` in one batch. -Dev: Reverts with `PolicyNotFound` when `policyId` does not exist. -Dev: Reverts with `IncompatiblePolicyType` when the policy is not BLOCKLIST. -Dev: Reverts with `Unauthorized` when the caller is not the current admin. -Dev: Reverts with `BatchSizeTooLarge` when `accounts.length` exceeds the registry limit. -Param: policyId Policy to update. -Param: blocked Membership state to apply to every account in the batch. -Param: accounts Accounts to update. +The change takes effect on the next `isAuthorized` query. Every token and composite policy that references `policyId` sees the updated result immediately — no second `updatePolicy` call is needed on any token. + +Batches are capped at `64` accounts (`MAX_BATCH_SIZE`). Exceeding this limit reverts with `BatchSizeTooLarge`. + +Emits `BlocklistUpdated(policyId, updater, blocked, accounts)` on success. ## Access Control -Callable by the policy admin for the target policy. +Callable only by the current admin of `policyId`. Any other caller reverts `Unauthorized`. -## Policy Interaction +## Revert Conditions -This is part of the singleton PolicyRegistry surface used by B20 policy scopes. +| Error | Condition | +|---|---| +| `Unauthorized()` | Caller is not the current policy admin. | +| `PolicyNotFound()` | `policyId` does not exist in the registry. | +| `IncompatiblePolicyType()` | The policy is not a `BLOCKLIST`. | +| `BatchSizeTooLarge(maxBatchSize)` | `accounts.length` exceeds 64. | ## Example -```solidity Usage Example -IPolicyRegistry(target).updateBlocklist(arg0, arg1, arg2); +```solidity Title Add accounts to a blocklist +// Add two addresses to an existing BLOCKLIST policy. +IPolicyRegistry(registry).updateBlocklist(sanctionsId, true, [Carol, Dave]); + +// Remove Carol from the blocklist. +IPolicyRegistry(registry).updateBlocklist(sanctionsId, false, [Carol]); ``` diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/update-composite.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/update-composite.mdx index 00ae58438..2a7ff229d 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/update-composite.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/update-composite.mdx @@ -1,10 +1,8 @@ --- title: "IPolicyRegistry.updateComposite" -description: "Generated B20 reference for updateComposite(uint64,uint64[])." +description: "Replaces a composite policy's child-policy set in full with a new list of simple policy IDs." --- - - ## Signature ```solidity IPolicyRegistry.sol @@ -18,30 +16,37 @@ function updateComposite(uint64 policyId, uint64[] calldata childPolicyIds) exte ## Description -Replaces a composite policy's child-policy set in full with `childPolicyIds`. -Dev: Reverts with `PolicyNotFound` when `policyId` does not exist. -Dev: Reverts with `IncompatiblePolicyType` when `policyId` is not a composite (UNION or INTERSECT). -Dev: Reverts with `Unauthorized` when the caller is not the current admin. A renounced composite -(admin `address(0)`) can never be updated. -Dev: Reverts with `ChildPoliciesOutsideOfRange` when `childPolicyIds.length` is not in -`[MIN_COMPOSITE_CHILD_POLICIES, MAX_COMPOSITE_CHILD_POLICIES]`; there is no clear-the-list -path (the composite child-policy range, not the 64-account batch limit). -Dev: Reverts with `PolicyNotFound` when any child policy does not exist. -Dev: Reverts with `InvalidChildPolicy` when any child policy is itself a composite -(not a simple policy). -Param: policyId Composite policy to update. -Param: childPolicyIds Complete new set of existing simple policy IDs. +Replaces a composite policy's child-policy set in full with `childPolicyIds`. There is no partial edit — the entire child set is replaced in one call. + +The write takes effect on the next `isAuthorized` call that references `policyId`. Every token that stores this policy ID sees the new result without a second `updatePolicy` call on the token. + +## Parameters + +| Name | Type | Description | +|---|---|---| +| `policyId` | `uint64` | Composite policy to update. Must exist and be a `UNION` or `INTERSECT` policy. | +| `childPolicyIds` | `uint64[]` | Complete new set of existing simple policy IDs. Count must be in `[MIN_COMPOSITE_CHILD_POLICIES, MAX_COMPOSITE_CHILD_POLICIES]` (2–4). | ## Access Control -Callable by the policy admin for the target policy. +Callable only by the current admin of `policyId`. A composite whose admin has been renounced (via `renounceAdmin`) can never be updated. + +## Revert Conditions + +| Error | Condition | +|---|---| +| `Unauthorized()` | Caller is not the current policy admin. | +| `PolicyNotFound()` | `policyId` does not exist, or any entry in `childPolicyIds` does not exist. | +| `IncompatiblePolicyType()` | `policyId` is not a composite (`UNION` or `INTERSECT`). | +| `ChildPoliciesOutsideOfRange()` | `childPolicyIds.length` is outside `[MIN_COMPOSITE_CHILD_POLICIES, MAX_COMPOSITE_CHILD_POLICIES]` (2–4). | +| `InvalidChildPolicy(childPolicyId)` | A child policy is itself a composite rather than a simple (`ALLOWLIST` or `BLOCKLIST`) policy. | -## Policy Interaction +## Events -This is part of the singleton PolicyRegistry surface used by B20 policy scopes. +Emits `CompositePolicyUpdated(policyId, updater, childPolicyIds)` on success. ## Example ```solidity Usage Example -IPolicyRegistry(target).updateComposite(arg0, arg1); +IPolicyRegistry(registry).updateComposite(gateId, newChildPolicyIds); ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/announce.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/announce.mdx index e89f74228..11054d8de 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/announce.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/announce.mdx @@ -1,14 +1,17 @@ --- title: "IB20Asset.announce" -description: "Generated B20 reference for announce(bytes[],string,string,string)." +description: "Wraps one or more asset calls in a disclosed corporate-action bracket, emitting Announcement and EndAnnouncement with the same id." --- - - ## Signature ```solidity IB20Asset.sol -function announce( bytes[] calldata internalCalls, string calldata id, string calldata description, string calldata uri ) external; +function announce( + bytes[] calldata internalCalls, + string calldata id, + string calldata description, + string calldata uri +) external; ``` | Field | Value | @@ -18,32 +21,59 @@ function announce( bytes[] calldata internalCalls, string calldata id, string ca ## Description -Posts a holder-impacting announcement and atomically dispatches each entry in -`internalCalls` via self-`delegatecall` (preserving `msg.sender`). Emits -`Announcement` then `EndAnnouncement` with the same `id`. Pass an empty -`internalCalls` for a pure disclosure. -Dev: Reverts with `AccessControlUnauthorizedAccount` when the caller does not hold `OPERATOR_ROLE`. -Dev: Reverts with `AnnouncementIdAlreadyUsed` when `id` has previously been consumed. -Dev: Reverts with `InternalCallMalformed` when an entry in `internalCalls` is shorter than four bytes. -Dev: Reverts with `AnnouncementInProgress` when an entry in `internalCalls` targets `announce` itself. -Dev: An inner call that raises a Solidity `Panic` (e.g. arithmetic overflow) propagates the -raw Panic unchanged; any other inner revert wraps as `InternalCallFailed(call)`. An inner -out-of-gas halts the whole call. -Param: internalCalls ABI-encoded calldata blobs executed in order via self-`delegatecall`; may be empty. -Param: id Caller-chosen announcement id; single-use over the token's lifetime. -Param: description Human-readable summary of the announcement. -Param: uri Off-chain URI containing the full announcement contents. +Emits `Announcement(caller, id, description, uri)`, executes every entry in `internalCalls` atomically via self-`delegatecall` (preserving `msg.sender`), then emits `EndAnnouncement(id)`. Pass an empty `internalCalls` array for a notice with no on-chain effect; both events still fire and `id` is consumed. + +Indexers pair open and close brackets by `id`. Every event emitted between the two logs belongs to the announced action. A state-changing call that appears outside this bracket was invoked directly, not through `announce`. + +## Parameters + +| Name | Type | Description | +|---|---|---| +| `internalCalls` | `bytes[]` | ABI-encoded calldata blobs executed in order via self-`delegatecall`. Each entry must be at least 4 bytes. May be empty. | +| `id` | `string` | Caller-chosen announcement identifier. Single-use over the token's lifetime. A successful `announce` consumes `id`; reuse reverts. | +| `description` | `string` | Human-readable summary shown to holders. The asset does not verify this string. | +| `uri` | `string` | Off-chain URI for the full announcement record. The asset does not verify this string. | ## Access Control -`OPERATOR_ROLE` gates this Asset call. +`OPERATOR_ROLE` gates this call. Any other caller reverts `AccessControlUnauthorizedAccount`. Inner calls keep their own role gates; the operator must hold every role the inner calls require (for example `MINT_ROLE` for `batchMint`, `BURN_ROLE` for `burnWithMemo`). + +## Revert Conditions + +| Error | Condition | +|---|---| +| `AccessControlUnauthorizedAccount(caller, OPERATOR_ROLE)` | Caller does not hold `OPERATOR_ROLE`. | +| `AnnouncementIdAlreadyUsed(id)` | A prior successful `announce` already consumed `id`. | +| `InternalCallMalformed(call)` | An entry in `internalCalls` is shorter than 4 bytes. | +| `AnnouncementInProgress()` | An inner call targets `announce` itself. | +| `InternalCallFailed(call)` | An inner call reverted with an ordinary revert. The underlying reason is not bubbled; replay the call directly to diagnose. | + +A Solidity `Panic` inside an inner call (for example arithmetic overflow) propagates raw and is not wrapped as `InternalCallFailed`. -## Policy Interaction +## Behavior Notes -No direct policy interaction. +- Execution order: emit `Announcement` → run inner calls → emit `EndAnnouncement`. If any inner call fails, the whole transaction reverts and `id` is not consumed. +- After a successful call, `isAnnouncementIdUsed(id)` returns `true`. +- `announce` is defined on **B20 Asset** only. Stablecoin has no announcements. +- Typical inner calls: `updateUIMultiplier`, `cancelUIMultiplierUpdate`, `batchMint`, `mintWithMemo`, `burnWithMemo`. ## Example -```solidity Usage Example -IB20Asset(target).announce(arg0, arg1, arg2, arg3); +```solidity Title Scheduled split wrapped in an announcement +bytes[] memory calls = new bytes[](1); +calls[0] = abi.encodeCall(IB20Asset.updateUIMultiplier, (2e18, effectiveAt)); + +asset.announce(calls, id, description, uri); +// Emits: Announcement → UIMultiplierUpdated → EndAnnouncement ``` + +```solidity Title Notice with no inner calls +asset.announce(new bytes[](0), id, description, uri); +// Emits: Announcement → EndAnnouncement +``` + +## Related + +- `isAnnouncementIdUsed(string id)` — selector `0xc0da474e` +- `Announcement(address indexed caller, string id, string description, string uri)` — topic0 `0xccebf8218a62875909564adef86a6f4df81503cb617221e793357d62f8e813f7` +- `EndAnnouncement(string id)` — topic0 `0x96d64dafe2c790596430196b982ad1da3221cb3b0f4e6e2df77f2e4f71a90037` diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/batch-mint.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/batch-mint.mdx index 11d3e9cfe..1e1a31c9c 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/batch-mint.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/batch-mint.mdx @@ -1,10 +1,8 @@ --- title: "IB20Asset.batchMint" -description: "Generated B20 reference for batchMint(address[],uint256[])." +description: "Mints tokens to multiple recipients in a single all-or-nothing call." --- - - ## Signature ```solidity IB20Asset.sol @@ -18,29 +16,55 @@ function batchMint(address[] calldata recipients, uint256[] calldata amounts) ex ## Description -Mints `amounts[i]` to `recipients[i]` in one call. All-or-nothing: any element -revert unwinds the whole transaction. Emits `Transfer(address(0), recipients[i], amounts[i])` -per element. -Dev: Reverts with `ContractPaused(MINT)` when `MINT` is paused. -Dev: Reverts with `AccessControlUnauthorizedAccount` when the caller does not hold `MINT_ROLE`. -Dev: Reverts with `LengthMismatch` when `recipients.length != amounts.length`. -Dev: Reverts with `EmptyBatch` when either array is empty. -Dev: Reverts with `InvalidReceiver` when any `recipients[i] == address(0)`. -Dev: Reverts with `PolicyForbids(MINT_RECEIVER_POLICY, ...)` when any recipient is not authorized. -Dev: Reverts with `SupplyCapExceeded` when the cumulative mint would exceed the cap. -Param: recipients Accounts receiving the minted tokens. -Param: amounts Per-recipient amounts, parallel to `recipients`. +Mints `amounts[i]` to `recipients[i]` in one call. All-or-nothing: any element revert unwinds the whole transaction. Emits `Transfer(address(0), recipients[i], amounts[i])` per element. + +## Parameters + +| Name | Type | Description | +|---|---|---| +| `recipients` | `address[]` | Accounts receiving the minted tokens. | +| `amounts` | `uint256[]` | Per-recipient amounts, parallel to `recipients`. | ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +Caller must hold `MINT_ROLE`. Reverts `AccessControlUnauthorizedAccount` otherwise. ## Policy Interaction -No direct policy interaction. +Each recipient must pass `MINT_RECEIVER_POLICY`. Reverts `PolicyForbids(MINT_RECEIVER_POLICY, ...)` for any recipient that does not. + +## Revert Conditions + +| Error | Condition | +|---|---| +| `AccessControlUnauthorizedAccount` | Caller does not hold `MINT_ROLE`. | +| `ContractPaused(MINT)` | The `MINT` operation is paused. | +| `LengthMismatch` | `recipients.length != amounts.length`. | +| `EmptyBatch` | Either array is empty. | +| `InvalidReceiver` | Any `recipients[i]` is `address(0)`. | +| `PolicyForbids(MINT_RECEIVER_POLICY, ...)` | Any recipient is not authorized by the mint receiver policy. | +| `SupplyCapExceeded` | Cumulative mint would exceed the supply cap. | ## Example -```solidity Usage Example -IB20Asset(target).batchMint(arg0, arg1); +```solidity Title Usage Example +address[] memory recipients = new address[](2); +recipients[0] = holderA; +recipients[1] = holderB; + +uint256[] memory amounts = new uint256[](2); +amounts[0] = 100e18; +amounts[1] = 200e18; + +IB20Asset(asset).batchMint(recipients, amounts); +``` + +Use `batchMint` inside an `announce` call to disclose a dividend issuance or additional mint as a corporate action: + +```solidity Title Announced Batch Mint +bytes[] memory calls = new bytes[](1); +calls[0] = abi.encodeCall(IB20Asset.batchMint, (recipients, amounts)); +asset.announce(calls, id, description, uri); ``` + +On success the asset emits `Announcement`, then one `Transfer(address(0), recipients[i], amounts[i])` per recipient, then `EndAnnouncement`. diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/effective-at.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/effective-at.mdx index 0ab7df83e..76a5efdd6 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/effective-at.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/effective-at.mdx @@ -1,6 +1,6 @@ --- title: "IB20Asset.effectiveAt" -description: "ERC-8056 pending multiplier timestamp reference." +description: "Returns the timestamp at which the pending UI multiplier becomes effective." --- ## Signature @@ -16,4 +16,27 @@ function effectiveAt() external view returns (uint256); ## Description -Returns the timestamp at which the pending UI multiplier becomes effective. +Returns the `block.timestamp` at which the pending UI multiplier becomes active. + +A live pending update exists while `effectiveAt() > block.timestamp`. Once `block.timestamp >= effectiveAt()`, `uiMultiplier()` returns the new multiplier automatically — no transaction is required and no event is emitted at maturation. + +After the flip, check for a new live pending update with `effectiveAt() > block.timestamp`. Do not check `effectiveAt() == 0`. + +## Behavior + +| State | Return value | +|---|---| +| No pending update | `0` | +| Pending update scheduled | Future timestamp set by `updateUIMultiplier` | +| Pending update matured | `0` (cleared after flip) | + + +Maturation is time-based and storage-free. Indexers must not wait for an on-chain event at `effectiveAt`. The `UIMultiplierUpdated` event fires when the update is **recorded**, not when it matures. + + +## Related + +- `uiMultiplier()` — returns the current effective multiplier. +- `newUIMultiplier()` — returns the scheduled multiplier value. +- `updateUIMultiplier(uint256, uint256)` — schedules a multiplier change and emits `UIMultiplierUpdated`. +- `cancelUIMultiplierUpdate()` — clears a live pending update before `effectiveAt`. diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/multiplier.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/multiplier.mdx index b6a76a397..1b5b561fe 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/multiplier.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/multiplier.mdx @@ -1,10 +1,8 @@ --- title: "IB20Asset.multiplier" -description: "Generated B20 reference for multiplier()." +description: "Returns the current UI multiplier, scaled to WAD_PRECISION, used to derive displayed share counts from raw ERC-20 balances." --- - - ## Signature ```solidity IB20Asset.sol @@ -18,15 +16,23 @@ function multiplier() external view returns (uint256); ## Description -The current multiplier, scaled to `WAD_PRECISION`. Holder balances are stored -as raw units; the multiplier scales them into a derived "scaled" view, similar -in shape to wstETH wrapping stETH. -Dev: Alias of the ERC-8056 `uiMultiplier()`. -Return: Current (effective) multiplier. +Returns the effective multiplier at `block.timestamp`, scaled to `WAD_PRECISION` (`1e18`). Holder balances are stored as raw ERC-20 units; the multiplier scales them into a derived UI view without rewriting those balances. + +This function is a deprecated alias of the ERC-8056 `uiMultiplier()`. Prefer `uiMultiplier()`. + +A default multiplier of `WAD_PRECISION` (`1e18`) means UI equals raw. A 2-for-1 split is encoded as `2e18`; a 1-for-2 reverse split is `5e17`. + +The UI balance for a holder is `balanceOf(account) * multiplier() / WAD_PRECISION`. Use `balanceOfUI(account)` to read that derived value directly. + +If a pending update has been scheduled via `updateUIMultiplier`, this function still returns the current multiplier until `block.timestamp >= effectiveAt()`. The pending value is readable from `newUIMultiplier()`. + + +This is a deprecated alias. Use `uiMultiplier()` (ERC-8056) instead. Both return the same value. + ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +View only. No role required. ## Policy Interaction @@ -35,5 +41,6 @@ No direct policy interaction. ## Example ```solidity Usage Example -IB20Asset(target).multiplier(); +IB20Asset(target).multiplier(); // deprecated alias +IB20Asset(target).uiMultiplier(); // preferred ERC-8056 name ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/operator-role.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/operator-role.mdx index e01d7c3b8..5379b9fd5 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/operator-role.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/operator-role.mdx @@ -1,10 +1,8 @@ --- title: "IB20Asset.OPERATOR_ROLE" -description: "Generated B20 reference for OPERATOR_ROLE()." +description: "Returns the OPERATOR_ROLE constant that gates multiplier scheduling and corporate action announcement on B20 Asset." --- - - ## Signature ```solidity IB20Asset.sol @@ -18,14 +16,20 @@ function OPERATOR_ROLE() external view returns (bytes32); ## Description -Required to call `announce`, `updateUIMultiplier`, `cancelUIMultiplierUpdate`, and -`updateMultiplier`. The metadata setters (`updateName`, `updateSymbol`, -`updateExtraMetadata`) are gated by the inherited `METADATA_ROLE` instead. -Return: Role constant. +Returns the `bytes32` role constant required to call: + +- `announce` — disclose a corporate action +- `updateUIMultiplier` — schedule a future multiplier change +- `cancelUIMultiplierUpdate` — cancel a live pending multiplier update +- `updateMultiplier` — immediate override (deprecated; emergency use only) + +The metadata setters (`updateName`, `updateSymbol`, `updateExtraMetadata`) are gated by `METADATA_ROLE`, not `OPERATOR_ROLE`. + +This role is Asset-only. B20 Stablecoin has no multiplier and does not expose `OPERATOR_ROLE` for these purposes. ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +Read-only view; returns a constant. No role required to call this function. ## Policy Interaction @@ -34,5 +38,5 @@ No direct policy interaction. ## Example ```solidity Usage Example -IB20Asset(target).OPERATOR_ROLE(); +bytes32 role = IB20Asset(target).OPERATOR_ROLE(); ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/scaled-balance-of.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/scaled-balance-of.mdx index 153b20770..152d0ad33 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/scaled-balance-of.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/scaled-balance-of.mdx @@ -1,9 +1,11 @@ --- title: "IB20Asset.scaledBalanceOf" -description: "Generated B20 reference for scaledBalanceOf(address)." +description: "Returns the UI (display) balance of an account by applying the current multiplier to its raw ERC-20 balance." --- - + +`scaledBalanceOf` is a deprecated alias. Prefer `balanceOfUI(account)` for new integrations. + ## Signature @@ -18,13 +20,34 @@ function scaledBalanceOf(address account) external view returns (uint256); ## Description -Convenience for `toUIAmount(balanceOf(account))`. -Param: account Account whose scaled balance is being queried. -Return: Scaled balance. +Equivalent to `toUIAmount(balanceOf(account))`. Returns the caller-visible share count after applying the current multiplier: `balanceOf(account) * uiMultiplier() / WAD_PRECISION`. + +The raw ERC-20 balance is unchanged. Only the displayed value scales with the multiplier. + +This function is a deprecated alias of `balanceOfUI(account)`. Prefer `balanceOfUI` for new code. + +## Parameters + +| Name | Type | Description | +|---|---|---| +| `account` | `address` | Account whose UI balance is being queried. | + +## Returns + +| Type | Description | +|---|---| +| `uint256` | UI balance: `balanceOf(account) * uiMultiplier() / WAD_PRECISION`. | + +## Multiplier Behavior + +- While a pending multiplier update exists (`effectiveAt() > block.timestamp`), this function still uses the **current** multiplier, not the scheduled one. +- After `block.timestamp >= effectiveAt()`, the new multiplier takes effect automatically on read — no transaction or event fires at maturation. +- Integer division rounds down. A round trip through `toUIAmount` then `fromUIAmount` can lose one unit in the last place when `uiMultiplier() != WAD_PRECISION`. +- This function applies only to B20 Asset. B20 Stablecoin has no multiplier. ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +Read-only. No role required. ## Policy Interaction @@ -33,5 +56,9 @@ No direct policy interaction. ## Example ```solidity Usage Example -IB20Asset(target).scaledBalanceOf(arg0); +// Deprecated alias — prefer balanceOfUI +uint256 uiBalance = IB20Asset(target).scaledBalanceOf(account); + +// Preferred +uint256 uiBalance = IB20Asset(target).balanceOfUI(account); ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/to-scaled-balance.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/to-scaled-balance.mdx index 2bc052f39..8eb8dedb5 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/to-scaled-balance.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/to-scaled-balance.mdx @@ -1,9 +1,11 @@ --- title: "IB20Asset.toScaledBalance" -description: "Generated B20 reference for toScaledBalance(uint256)." +description: "Converts a raw token balance to its UI-scaled view using the current multiplier." --- - + +`toScaledBalance` is a deprecated alias of `toUIAmount`. Prefer `toUIAmount(raw)` for new integrations. + ## Signature @@ -18,13 +20,31 @@ function toScaledBalance(uint256 rawBalance) external view returns (uint256); ## Description -Converts a raw balance to its scaled view: `rawBalance * multiplier / WAD_PRECISION`. -Param: rawBalance Raw token amount to scale. -Return: Scaled balance at the current multiplier. +Converts a raw balance to its UI (scaled) view: `rawBalance * uiMultiplier() / WAD_PRECISION`. + +This is a deprecated alias of `toUIAmount`. The two functions are equivalent. Use `toUIAmount` in new code. + +The result is the amount that wallets and indexers display as a share count after a stock split. Raw ERC-20 balances, `transfer` amounts, `totalSupply`, and allowances are unaffected by the multiplier and remain raw. + +Integer division rounds down. A round trip through `toUIAmount` then `fromUIAmount` can lose one unit in the last place (ULP) when `uiMultiplier() != WAD_PRECISION`. + +Multipliers are Asset-only. Stablecoin has no multiplier. + +## Parameters + +| Name | Type | Description | +|---|---|---| +| `rawBalance` | `uint256` | Raw token amount to convert. | + +## Returns + +| Type | Description | +|---|---| +| `uint256` | UI balance at the effective multiplier: `rawBalance * uiMultiplier() / WAD_PRECISION`. | ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +Read-only. No role required. ## Policy Interaction @@ -33,5 +53,16 @@ No direct policy interaction. ## Example ```solidity Usage Example -IB20Asset(target).toScaledBalance(arg0); +// Deprecated alias — prefer toUIAmount +uint256 ui = IB20Asset(target).toScaledBalance(rawBalance); + +// Preferred equivalent +uint256 ui = IB20Asset(target).toUIAmount(rawBalance); ``` + +## Related + +- `toUIAmount` — preferred replacement. +- `fromUIAmount` (`toRawBalance`) — inverse conversion. +- `balanceOfUI` (`scaledBalanceOf`) — applies the same formula to a holder's stored balance. +- `uiMultiplier` — returns the effective multiplier at `block.timestamp`. diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/to-ui-amount.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/to-ui-amount.mdx index deaff04f5..e20d3e611 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/to-ui-amount.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/to-ui-amount.mdx @@ -1,6 +1,6 @@ --- title: "IB20Asset.toUIAmount" -description: "ERC-8056 raw-to-UI conversion reference." +description: "Converts a raw ERC-20 balance to its UI representation using the effective multiplier." --- ## Signature @@ -16,4 +16,27 @@ function toUIAmount(uint256 rawAmount) external view returns (uint256); ## Description -Converts `rawAmount` to its UI representation at the effective multiplier. +Converts `rawAmount` to its UI representation at the effective multiplier. The formula is `rawAmount * uiMultiplier() / WAD_PRECISION`, where `WAD_PRECISION` is `1e18`. Integer division rounds down, so a round trip through `toUIAmount` then `fromUIAmount` can lose one unit in the last place (ULP) when the multiplier differs from `WAD_PRECISION`. + +The effective multiplier is time-dependent. If a pending update has been scheduled via `updateUIMultiplier` and `block.timestamp >= effectiveAt`, the new multiplier is used even though no storage write or event fires at that moment. Call `uiMultiplier()` to inspect the value this function applies. + +`toScaledBalance` is a deprecated alias of this function. Prefer `toUIAmount`. + +## Parameters + +| Name | Type | Description | +|---|---|---| +| `rawAmount` | `uint256` | Raw ERC-20 balance to convert. | + +## Returns + +| Type | Description | +|---|---| +| `uint256` | UI amount: `rawAmount * uiMultiplier() / 1e18`, rounded down. | + +## Notes + +- This function is Asset-only. Stablecoin has no multiplier. +- A default multiplier of `1e18` means UI equals raw. +- A 2-for-1 split sets the multiplier to `2e18`; a 1-for-2 reverse split sets it to `5e17`. +- The multiplier is an absolute value, not a ratio applied to the current one. diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/ui-multiplier.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/ui-multiplier.mdx index ddf7c4482..769ce150d 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/ui-multiplier.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/ui-multiplier.mdx @@ -1,6 +1,6 @@ --- title: "IB20Asset.uiMultiplier" -description: "ERC-8056 UI multiplier reference." +description: "Returns the current ERC-8056 UI multiplier used to derive the displayed share count from raw ERC-20 balances." --- ## Signature @@ -16,4 +16,28 @@ function uiMultiplier() external view returns (uint256); ## Description -Returns the current ERC-8056 UI multiplier, represented with 18 decimals (`1e18 = 1.0`). +Returns the effective UI multiplier at `block.timestamp`, represented with 18 decimals (`1e18 = 1.0`). + +Raw ERC-20 balances are never rewritten during a split. The multiplier changes the displayed scale only. A wallet or indexer derives the UI share count as: + +``` +ui = balanceOf(account) * uiMultiplier() / 1e18 +``` + +A default (no-split) state returns `1e18`. A 2-for-1 split returns `2e18`. A 1-for-2 reverse split returns `5e17`. + +## Timing + +`uiMultiplier()` reflects a scheduled change **at or after** `effectiveAt`, not when `updateUIMultiplier` is called. Before `effectiveAt`, it returns the current multiplier. There is no event emitted at the flip — indexers must not wait for a log at `effectiveAt`. + +To inspect a live pending update, read `newUIMultiplier()` and `effectiveAt()`. A pending update is active while `effectiveAt() > block.timestamp`. + +## Related + +- `newUIMultiplier()` — the scheduled multiplier value, if any. +- `effectiveAt()` — the timestamp at which the pending multiplier takes effect. +- `balanceOfUI(account)` — convenience view: `balanceOf(account) * uiMultiplier() / 1e18`. +- `totalSupplyUI()` — same formula applied to `totalSupply()`. +- `toUIAmount(raw)` / `fromUIAmount(ui)` — convert single amounts at the current multiplier. +- `updateUIMultiplier(newMultiplier, effectiveAt)` — schedules a future multiplier change. +- `multiplier()` — deprecated alias of `uiMultiplier()`. diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/update-multiplier.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/update-multiplier.mdx index 1574a8f72..2e4bfa6e8 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/update-multiplier.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/update-multiplier.mdx @@ -1,9 +1,11 @@ --- title: "IB20Asset.updateMultiplier" -description: "Generated B20 reference for updateMultiplier(uint256)." +description: "Deprecated instant multiplier override that sets the multiplier immediately and clears any pending scheduled update." --- - + +`updateMultiplier` is deprecated. Use `updateUIMultiplier` for routine stock splits and corporate actions. Reserve this function for emergency overrides only. + ## Signature @@ -18,16 +20,37 @@ function updateMultiplier(uint256 newMultiplier) external; ## Description -DEPRECATED. Instant failsafe / emergency override — sets the current multiplier immediately and -cancels any live pending update without a scheduling window. Emits both `MultiplierUpdated` and -`UIMultiplierUpdated`. Prefer `updateUIMultiplier` for routine corporate actions. -Dev: Reverts with `AccessControlUnauthorizedAccount` when the caller does not hold `OPERATOR_ROLE`. -Dev: Reverts with `InvalidMultiplier` when `newMultiplier` is zero or above `type(uint128).max`. -Param: newMultiplier New multiplier scaled to `WAD_PRECISION`; must be in `(0, type(uint128).max]`. +Applies `newMultiplier` immediately at `block.timestamp` and clears any live pending update without waiting for a scheduling window. Use this only when a live pending update is wrong and waiting for `effectiveAt` is not acceptable. Prefer `cancelUIMultiplierUpdate` followed by `updateUIMultiplier` when the fix can use a future timestamp. + +The multiplier is an 18-decimal WAD: `1e18` is `1.0` (`WAD_PRECISION`). A 2-for-1 split is `2e18`. A reverse split is a value below `1e18`. ## Access Control -`OPERATOR_ROLE` gates this Asset call. +Caller must hold `OPERATOR_ROLE`. Any other caller reverts `AccessControlUnauthorizedAccount(caller, OPERATOR_ROLE)`. + +## Parameters + +| Name | Type | Description | +|---|---|---| +| `newMultiplier` | `uint256` | New multiplier scaled to `WAD_PRECISION`. Must be in `(0, MAX_UI_MULTIPLIER]`. | + +## Reverts + +| Error | Condition | +|---|---| +| `AccessControlUnauthorizedAccount(caller, OPERATOR_ROLE)` | Caller does not hold `OPERATOR_ROLE`. | +| `InvalidMultiplier()` | `newMultiplier` is zero or above `MAX_UI_MULTIPLIER`. | + +## Events + +Event order depends on whether a pending update exists at call time: + +| Situation | Events (in order) | +|---|---| +| Live pending (`effectiveAt > block.timestamp`) | `UIMultiplierUpdateCancelled`, then `MultiplierUpdated(new)`, then `UIMultiplierUpdated(old, new, block.timestamp)` | +| Matured or no pending | `MultiplierUpdated(new)`, then `UIMultiplierUpdated(old, new, block.timestamp)` | + +`MultiplierUpdated` is itself deprecated. Integrators should process only `UIMultiplierUpdated` to avoid handling the same update twice. ## Policy Interaction @@ -36,5 +59,7 @@ No direct policy interaction. ## Example ```solidity Usage Example -IB20Asset(target).updateMultiplier(arg0); +// Emergency override: apply the correct multiplier immediately, +// clearing any live pending update. +IB20Asset(asset).updateMultiplier(correctMultiplier); ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/wad-precision.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/wad-precision.mdx index d72a48835..ffbf79f38 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/wad-precision.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/wad-precision.mdx @@ -1,10 +1,8 @@ --- title: "IB20Asset.WAD_PRECISION" -description: "Generated B20 reference for WAD_PRECISION()." +description: "Fixed-point precision constant used to scale the B20 Asset UI multiplier." --- - - ## Signature ```solidity IB20Asset.sol @@ -18,12 +16,15 @@ function WAD_PRECISION() external view returns (uint256); ## Description -Fixed-point precision used to scale `multiplier`. Equal to `1e18`. -Return: Precision constant. +Returns `1e18`, the fixed-point denominator used to scale the UI multiplier. + +The UI balance formula is `raw * multiplier / WAD_PRECISION`. A multiplier of `1e18` means UI equals raw (1.0×). A 2-for-1 split uses `2e18`; a 1-for-2 reverse split uses `5e17`. + +This constant applies only to B20 Asset. B20 Stablecoin has no multiplier. ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +View-only. No role required. ## Policy Interaction @@ -32,5 +33,6 @@ No direct policy interaction. ## Example ```solidity Usage Example -IB20Asset(target).WAD_PRECISION(); +uint256 precision = IB20Asset(target).WAD_PRECISION(); // 1e18 +uint256 uiBalance = rawBalance * multiplier / precision; ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20-factory/create-b20.mdx b/docs/specifications/b20/reference/interfaces/ib20-factory/create-b20.mdx index 5dbb1f35b..a25563d27 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-factory/create-b20.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-factory/create-b20.mdx @@ -1,10 +1,8 @@ --- title: "IB20Factory.createB20" -description: "Generated B20 reference for createB20(uint8,bytes32,bytes,bytes[])." +description: "Creates a B20 token of a given variant at a deterministic address, seals its identity, and runs optional bootstrap calls in the same transaction." --- - - ## Signature ```solidity IB20Factory.sol @@ -18,44 +16,56 @@ function createB20(B20Variant variant, bytes32 salt, bytes calldata params, byte ## Description -Creates a B-20 token of the given `variant` at the deterministic address derived -from `(variant, msg.sender, salt)`, then dispatches each entry in `initCalls` on -the new token. Emits `B20Created`. -Dev: Reverts with `NonPayable` when ETH is attached to the call. -Dev: Reverts with IActivationRegistry.FeatureNotActivated when the variant feature is not activated. -Dev: Reverts with `InvalidVariant` when `variant` is outside the `B20Variant` range. -Dev: Reverts with `UnsupportedVersion` when the leading `version` byte in `params` is unrecognized for `variant`. -Dev: Reverts with `MissingRequiredField` when a required string field is empty (e.g. stablecoin `currency`). -Dev: Reverts with `InvalidCurrency` when a stablecoin `currency` is non-empty but contains a non-`A`-`Z` byte. -Dev: Reverts with `InvalidDecimals` when an asset `decimals` is outside `[B20Constants.MIN_ASSET_DECIMALS, B20Constants.MAX_ASSET_DECIMALS]`. -Dev: Reverts with `TokenAlreadyExists` when a token already exists at the derived address. -Dev: Reverts with `InitCallFailed` (or the bubbled inner reason) when any entry in `initCalls` reverts. -Dev: Each `initCall` executes on the new token within the creation (bootstrap) window, during which -factory-originated calls bypass the token's role gates and its transfer-side policy gates -(`TRANSFER_SENDER_POLICY`, `TRANSFER_RECEIVER_POLICY`, `TRANSFER_EXECUTOR_POLICY`) — so admin-gated -setup (e.g. `grantRole`, `updatePolicy`, `updateSupplyCap`) and bootstrap transfers succeed without -the factory holding any role. The bypass is deliberately NOT total: -- `MINT_RECEIVER_POLICY` is ALWAYS enforced, including for factory-originated mints, so new supply is -never issued to a policy-denied recipient even at creation. An `initCalls` bundle that sets a -restrictive `MINT_RECEIVER_POLICY` and then mints to a non-authorized account reverts -`PolicyForbids(MINT_RECEIVER_POLICY, ...)` (bubbled out of `createB20`). -- Pause is never bypassed. It defaults to nothing-paused at creation, so a start-paused -configuration must sequence its `pause(...)` call last among the `initCalls`. -- Token invariants (supply-cap math, balance accounting) are never bypassed. -The window closes when `createB20` returns; the factory retains no persisted access. -Param: variant Which variant struct `params` decodes as. -Param: salt Caller-chosen salt for deterministic address derivation. -Param: params ABI-encoded variant-specific creation struct, leading with the version byte. -Param: initCalls Bootstrap calls invoked on the new token after identity is sealed. -Return: token The address of the newly created token. +Creates a B20 token of the given `variant` at the deterministic address derived from `(variant, msg.sender, salt)`. The Factory encodes the variant choice into the token address: byte `[0]` is `0xB2`, byte `[10]` is the variant discriminant (`0x00` for Asset, `0x01` for Stablecoin). The Factory then plants a `0xef` bytecode stub at that address — the stub the node reads to recognize a live B20 — seals the token's identity (name, symbol, decimals, and variant-specific fields), emits `B20Created`, grants the initial admin role (skipped when `address(0)` is passed), and dispatches each entry in `initCalls` on the new token. Returns the token address. + +The type is chosen once. After `createB20` returns, the variant encoded in the address cannot change. The Factory retains no persisted access after the call returns. + +## Parameters + +| Name | Type | Description | +|---|---|---| +| `variant` | `B20Variant` (`uint8`) | Which variant to create: `ASSET` (`0x00`) or `STABLECOIN` (`0x01`). Determines which `params` struct to decode and which native logic the node dispatches to. | +| `salt` | `bytes32` | Caller-chosen salt for deterministic address derivation. | +| `params` | `bytes` | ABI-encoded variant-specific creation struct, leading with a `version` byte (currently `1`). For Asset: `B20AssetCreateParams` (name, symbol, `initialAdmin`, `decimals`). For Stablecoin: `B20StablecoinCreateParams` (name, symbol, `initialAdmin`, `currency`). | +| `initCalls` | `bytes[]` | Bootstrap calls invoked on the new token after identity is sealed. May be empty. | + +## Returns + +| Name | Type | Description | +|---|---|---| +| `token` | `address` | Address of the newly created token. | + +## Revert Conditions + +| Error | Condition | +|---|---| +| `NonPayable` | ETH is attached to the call. | +| `FeatureNotActivated` | The variant feature (`B20Asset` or `B20Stablecoin`) is not activated in the Activation Registry. | +| `InvalidVariant` | `variant` is outside the `B20Variant` range. | +| `UnsupportedVersion` | The leading `version` byte in `params` is unrecognized for the given `variant`. | +| `MissingRequiredField` | A required string field is empty — for example, Stablecoin `currency`. | +| `InvalidCurrency` | Stablecoin `currency` is non-empty but contains a byte outside `A`–`Z`. | +| `InvalidDecimals` | Asset `decimals` is outside `[B20Constants.MIN_ASSET_DECIMALS, B20Constants.MAX_ASSET_DECIMALS]` (i.e., outside `[6, 18]`). | +| `TokenAlreadyExists` | A token already exists at the derived address. Reusing `(variant, sender, salt)` always hits this. | +| `InitCallFailed` | An entry in `initCalls` reverted. The inner reason is bubbled when available. | + +## Bootstrap Window + +During the `createB20` call, factory-originated calls bypass the token's role gates and its transfer-side policy gates (`TRANSFER_SENDER_POLICY`, `TRANSFER_RECEIVER_POLICY`, `TRANSFER_EXECUTOR_POLICY`). This lets `initCalls` perform admin setup — `grantRole`, `updatePolicy`, `updateSupplyCap`, bootstrap transfers — without the Factory holding any role. The bypass is not total: + +- **`MINT_RECEIVER_POLICY` is always enforced**, including for factory-originated mints. An `initCalls` bundle that sets a restrictive `MINT_RECEIVER_POLICY` and then mints to a non-authorized account reverts `PolicyForbids(MINT_RECEIVER_POLICY, ...)`, bubbled out of `createB20`. +- **Pause is never bypassed.** Pause defaults to nothing-paused at creation. To start paused, sequence the `pause(...)` call last among `initCalls`. +- **Token invariants are never bypassed.** Supply-cap math and balance accounting always apply. + +The window closes when `createB20` returns. ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +No role required. Any caller may invoke `createB20`. The caller's address is part of the address derivation `(variant, msg.sender, salt)`. ## Policy Interaction -No direct policy interaction. +No direct policy interaction at the Factory level. Policy gates on the new token are enforced during `initCalls` as described in the bootstrap window rules above. ## Example diff --git a/docs/specifications/b20/reference/interfaces/ib20-factory/is-b20-initialized.mdx b/docs/specifications/b20/reference/interfaces/ib20-factory/is-b20-initialized.mdx index dfd6e85c5..677e4df7e 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-factory/is-b20-initialized.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-factory/is-b20-initialized.mdx @@ -1,10 +1,8 @@ --- title: "IB20Factory.isB20Initialized" -description: "Generated B20 reference for isB20Initialized(address)." +description: "Returns whether the Factory has fully initialized a B20 token at the given address." --- - - ## Signature ```solidity IB20Factory.sol @@ -18,21 +16,36 @@ function isB20Initialized(address token) external view returns (bool); ## Description -Returns whether `createB20` has run to completion at `token`. Flips exactly once, -the moment the creating `createB20` call returns. Never reverts. -Param: token Address to check. -Return: Whether `token` is an initialized B-20. +Returns `true` if and only if `token` bears the `0xB2` address prefix **and** the `0xef` bytecode stub that `createB20` plants when it creates a token. This is the stronger existence check: `isB20(address)` tests the prefix alone and can return `true` for an address the Factory has never touched, whereas `isB20Initialized` confirms the Factory has run to completion at that address. + +The flag flips exactly once — the moment the creating `createB20` call returns. It never reverts. + + +Before `createB20` is called, the predicted token address matches the `0xB2` prefix but carries no stub. Calls to that address are no-ops. Only after `createB20` returns does the `0xef` stub exist, enabling dynamic routing to the token's native logic. + + +## Parameters + +| Name | Type | Description | +|---|---|---| +| `token` | `address` | Address to check. | + +## Returns + +| Type | Description | +|---|---| +| `bool` | `true` if `token` is a fully initialized B20 (prefix + stub present); `false` otherwise. | ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +View function. No role required. -## Policy Interaction +## Revert Conditions -No direct policy interaction. +Never reverts. ## Example ```solidity Usage Example -IB20Factory(target).isB20Initialized(arg0); +IB20Factory(factoryAddress).isB20Initialized(tokenAddress); ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20-factory/is-b20.mdx b/docs/specifications/b20/reference/interfaces/ib20-factory/is-b20.mdx index b38b219ab..4e1b72d45 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-factory/is-b20.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-factory/is-b20.mdx @@ -1,10 +1,8 @@ --- title: "IB20Factory.isB20" -description: "Generated B20 reference for isB20(address)." +description: "Returns whether an address matches the B20 address prefix, recovered from the address itself without consulting a registry." --- - - ## Signature ```solidity IB20Factory.sol @@ -18,20 +16,34 @@ function isB20(address token) external view returns (bool); ## Description -Returns whether `token` was created by this factory, recovered from the address prefix. Never reverts. -Param: token Address to check. -Return: Whether `token` matches the B-20 address prefix. +Returns `true` when `token` bears the `0xB2` address prefix (byte `[0]` is `0xB2`, bytes `[1:9]` are zero). The check reads the address directly; it does not consult a registry or any on-chain state. Never reverts. + + +`isB20` is a prefix-only check. It can return `true` for an address the Factory has not yet created. Use `isB20Initialized` when you need to confirm the token is live — that stronger check also requires the `0xef` bytecode stub the Factory plants during `createB20`. + + +## Parameters + +| Name | Type | Description | +|---|---|---| +| `token` | `address` | Address to check. | + +## Returns + +| Type | Description | +|---|---| +| `bool` | `true` if `token` matches the B20 address prefix; `false` otherwise. | ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +View — no access restriction. ## Policy Interaction -No direct policy interaction. +No policy interaction. ## Example ```solidity Usage Example -IB20Factory(target).isB20(arg0); +IB20Factory(factory).isB20(token); ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20/grant-role.mdx b/docs/specifications/b20/reference/interfaces/ib20/grant-role.mdx index 6a532cf0b..e6b3bb6fc 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/grant-role.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/grant-role.mdx @@ -1,10 +1,8 @@ --- title: "IB20.grantRole" -description: "Generated B20 reference for grantRole(bytes32,address)." +description: "Grants a role to an account on a B20 token, subject to the caller holding the role's admin role." --- - - ## Signature ```solidity IB20.sol @@ -18,22 +16,30 @@ function grantRole(bytes32 role, address account) external; ## Description -Grants `role` to `account`. Emits `RoleGranted`. +Grants `role` to `account`. If `account` does not already hold `role`, emits `RoleGranted(role, account, sender)`. If `account` already holds `role`, the call is a no-op and emits nothing. + +The caller must hold the admin role of `role`. On a fresh token every role's admin is `DEFAULT_ADMIN_ROLE`. That mapping can be changed with `setRoleAdmin`. After `renounceLastAdmin` is called, no address holds `DEFAULT_ADMIN_ROLE` and `grantRole` becomes permanently unreachable for any role whose admin chain includes `DEFAULT_ADMIN_ROLE`. + +Any number of addresses can hold the same role. There is no single-holder slot. ## Parameters | Parameter | Description | |---|---| -| `role` | Role to grant. | -| `account` | Recipient. | +| `role` | The `bytes32` role identifier to grant. | +| `account` | The address that will receive the role. | ## Reverts -- `AccessControlUnauthorizedAccount` when the caller does not hold the admin role for `role`, or when the token has been transitioned to admin-less via `renounceLastAdmin` (admin-resurrection guard). +| Error | Condition | +|---|---| +| `AccessControlUnauthorizedAccount(account, neededRole)` | Caller does not hold the admin role for `role`, including when the token has no admins after `renounceLastAdmin`. | ## Access Control -Uses the B20 access-control role graph. Factory initCalls bypass role gates during creation. +The caller must hold `getRoleAdmin(role)`. By default that is `DEFAULT_ADMIN_ROLE` for every role. After `setRoleAdmin(role, newAdminRole)` is called, the caller must hold `newAdminRole` instead. + +Factory `initCall`s bypass role gates during token creation. `createB20` uses this path to grant `DEFAULT_ADMIN_ROLE` to `initialAdmin` without a role check. ## Policy Interaction @@ -42,5 +48,5 @@ No direct policy interaction. ## Example ```solidity Usage Example -IB20(target).grantRole(role, account); +IB20(target).grantRole(MINT_ROLE, minterAddress); ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20/is-paused.mdx b/docs/specifications/b20/reference/interfaces/ib20/is-paused.mdx index c268fc8a2..dc4a69aad 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/is-paused.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/is-paused.mdx @@ -1,10 +1,8 @@ --- title: "IB20.isPaused" -description: "Generated B20 reference for isPaused(PausableFeature)." +description: "Returns whether a specific pausable feature is currently paused on a B20 token." --- - - ## Signature ```solidity IB20.sol @@ -18,28 +16,48 @@ function isPaused(PausableFeature feature) external view returns (bool); ## Description -Whether `feature` is currently paused. O(1). +Returns whether `feature` is currently paused. O(1). + +`PausableFeature` is an enum with four independent values: + +| Value | Ordinal | Gates | +|---|---|---| +| `TRANSFER` | 0 | `transfer`, `transferFrom`, and memo variants | +| `MINT` | 1 | `mint`, `mintWithMemo`, and `batchMint` | +| `BURN` | 2 | `burn`, `burnWithMemo`, and the deprecated `burnBlocked` | +| `SEIZE` | 3 | `seizeWithMemo` | + +The paused state for all features is stored as a bitmask in a single storage word; each feature's bit is its ordinal. `isPaused` reads that bit for the requested feature. ## Parameters -| Parameter | Description | -|---|---| -| `feature` | Feature to query. | +| Parameter | Type | Description | +|---|---|---| +| `feature` | `PausableFeature` | The feature to query. | ## Returns -Whether `feature` is paused. +`true` if `feature` is paused; `false` otherwise. ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +View function — no role required. ## Policy Interaction No direct policy interaction. +## Behavior Notes + +- A paused feature blocks the operations listed in the table above regardless of whether the caller holds the relevant role. For example, a `MINT_ROLE` holder cannot mint while `MINT` is paused. +- If an operation is attempted while its feature is paused, the transaction reverts with `ContractPaused(feature)`. +- Use `pausedFeatures()` to read the full current paused set in one call. + ## Example ```solidity Usage Example -IB20(target).isPaused(feature); +bool mintPaused = IB20(target).isPaused(PausableFeature.MINT); +if (mintPaused) { + // minting is currently blocked +} ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20/pause.mdx b/docs/specifications/b20/reference/interfaces/ib20/pause.mdx index e9fecf819..a3d246397 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/pause.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/pause.mdx @@ -1,10 +1,8 @@ --- title: "IB20.pause" -description: "Generated B20 reference for pause(PausableFeature[])." +description: "Pauses one or more token operation classes, blocking the gated functions until each feature is unpaused." --- - - ## Signature ```solidity IB20.sol @@ -18,22 +16,34 @@ function pause(PausableFeature[] calldata features) external; ## Description -Pauses each of `features`. Additive: features already paused remain paused; duplicates within the call are idempotent. Emits `Paused`. +Pauses each feature in `features`. The call is additive: features already paused remain paused, and duplicates within the array are silent no-ops. Emits `Paused(updater, features)` with the exact array passed. ## Parameters | Parameter | Description | |---|---| -| `features` | Features to pause. | +| `features` | One or more `PausableFeature` values to pause. Must be non-empty. | + +## PausableFeature values + +| Value | Gates | +|---|---| +| `TRANSFER` | `transfer`, `transferFrom`, and memo variants | +| `MINT` | `mint`, `mintWithMemo`, and Asset `batchMint` | +| `BURN` | `burn`, `burnWithMemo`, and deprecated `burnBlocked` | +| `SEIZE` | `seizeWithMemo` | + +Each feature corresponds to one bit in a single storage word (`TRANSFER` = bit 0, `MINT` = bit 1, `BURN` = bit 2, `SEIZE` = bit 3). `ALL_FEATURES_PAUSED` (`15`) means all four bits are set. ## Reverts -- `AccessControlUnauthorizedAccount` when the caller does not hold `PAUSE_ROLE`. -- `EmptyFeatureSet` when `features.length == 0`. +- `AccessControlUnauthorizedAccount(account, neededRole)` — caller does not hold `PAUSE_ROLE`. +- `EmptyFeatureSet()` — `features.length == 0`. +- `ContractPaused(feature)` — emitted by downstream calls when a paused feature is exercised; not thrown by `pause` itself. ## Access Control -`PAUSE_ROLE` gates this call. +`PAUSE_ROLE` gates this call. `PAUSE_ROLE` and `UNPAUSE_ROLE` are separate roles, so the account that pauses need not be the account that resumes. ## Policy Interaction @@ -42,5 +52,9 @@ No direct policy interaction. ## Example ```solidity Usage Example +// Pause MINT and BURN in one call; TRANSFER remains live. +PausableFeature[] memory features = new PausableFeature[](2); +features[0] = PausableFeature.MINT; +features[1] = PausableFeature.BURN; IB20(target).pause(features); ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20/paused-features.mdx b/docs/specifications/b20/reference/interfaces/ib20/paused-features.mdx index 415d0ac47..f7bfddac1 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/paused-features.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/paused-features.mdx @@ -1,10 +1,8 @@ --- title: "IB20.pausedFeatures" -description: "Generated B20 reference for pausedFeatures()." +description: "Returns the set of PausableFeature values currently paused on this token." --- - - ## Signature ```solidity IB20.sol @@ -18,22 +16,34 @@ function pausedFeatures() external view returns (PausableFeature[] memory); ## Description -The `PausableFeature`s currently paused on this token. Order is implementation-defined; callers should treat the result as a set. +Returns every `PausableFeature` that is currently paused on this token. The four possible values are `TRANSFER`, `MINT`, `BURN`, and `SEIZE`. The paused set is stored as one bit per feature in a single storage word; this function decodes that bitmap into an array. Order is implementation-defined — treat the result as a set. + +To check a single feature, prefer `isPaused(feature)`. ## Returns -Currently-paused features. +| Name | Type | Description | +|---|---|---| +| _(unnamed)_ | `PausableFeature[] memory` | Every feature currently paused. Empty array when nothing is paused. | ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +View — no role required. ## Policy Interaction No direct policy interaction. +## Behavior Notes + +- The returned array reflects the state at the block the call executes in. +- `ALL_FEATURES_PAUSED` (`15`) means all four bits are set; this function returns all four enum members in that case. +- Pausing is controlled by `pause(PausableFeature[])` (requires `PAUSE_ROLE`) and reversed by `unpause(PausableFeature[])` (requires `UNPAUSE_ROLE`). When a feature is paused, operations it gates revert `ContractPaused(feature)`. + ## Example ```solidity Usage Example -IB20(target).pausedFeatures(); +PausableFeature[] memory paused = IB20(target).pausedFeatures(); +// paused is empty when no features are paused +// paused contains MINT when minting is frozen ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20/policy-id.mdx b/docs/specifications/b20/reference/interfaces/ib20/policy-id.mdx index 4465924b6..fca63e0bd 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/policy-id.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/policy-id.mdx @@ -1,10 +1,8 @@ --- title: "IB20.policyId" -description: "Generated B20 reference for policyId(bytes32)." +description: "Returns the policy ID currently bound to a given policy scope on a B20 token." --- - - ## Signature ```solidity IB20.sol @@ -18,32 +16,44 @@ function policyId(bytes32 policyScope) external view returns (uint64); ## Description -The current policy ID configured for `policyScope`. Returns `0` (always-allow built-in) for any slot that has never been assigned. +Returns the policy ID stored for `policyScope`. If the scope has never been assigned, this returns `0`, which is the `ALWAYS_ALLOW` built-in sentinel — every address is authorized. ## Parameters | Parameter | Description | |---|---| -| `policyScope` | Policy slot scope. | +| `policyScope` | The policy scope slot to query (for example, `TRANSFER_RECEIVER_POLICY` or `MINT_RECEIVER_POLICY`). | ## Returns -Configured policy ID. +The `uint64` policy ID currently bound to the scope. `0` means `ALWAYS_ALLOW`. ## Reverts -- `UnsupportedPolicyType` when `policyScope` is not recognized by this token. +- `UnsupportedPolicyType(policyScope)` — when `policyScope` is not a slot this token supports. ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +View function. No role required. + +## Policy Scopes + +The following scopes are recognized by a B20 token. Pass one of these as `policyScope`: -## Policy Interaction +| Scope | Checked on | Account checked | +|---|---|---| +| `TRANSFER_SENDER_POLICY` | `transfer`, `transferFrom`, and memo variants | `from` | +| `TRANSFER_RECEIVER_POLICY` | `transfer`, `transferFrom`, and memo variants | `to` | +| `TRANSFER_EXECUTOR_POLICY` | `transferFrom` and `transferFromWithMemo` when `msg.sender != from` | `msg.sender` | +| `MINT_RECEIVER_POLICY` | `mint`, `mintWithMemo`, `batchMint` | `to` | +| `SEIZE_HOLDER_POLICY` | `seizeWithMemo` | `from` | +| `SEIZE_RECEIVER_POLICY` | `seizeWithMemo` | `to` | -Reads or writes a token policy-scope pointer into the PolicyRegistry. +An unset scope reads as `0` (`ALWAYS_ALLOW`). To bind a different policy, the token admin calls `updatePolicy(policyScope, newPolicyId)`. ## Example ```solidity Usage Example uint64 id = IB20(token).policyId(B20Constants.MINT_RECEIVER_POLICY); +// id == 0 means ALWAYS_ALLOW (no mint restriction) ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20/renounce-role.mdx b/docs/specifications/b20/reference/interfaces/ib20/renounce-role.mdx index c0a0ed7a8..12268769b 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/renounce-role.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/renounce-role.mdx @@ -1,10 +1,8 @@ --- title: "IB20.renounceRole" -description: "Generated B20 reference for renounceRole(bytes32,address)." +description: "Lets the caller remove a role from themselves. Reverts if the confirmation argument does not match the caller or if the caller is the last default admin." --- - - ## Signature ```solidity IB20.sol @@ -18,23 +16,23 @@ function renounceRole(bytes32 role, address callerConfirmation) external; ## Description -Caller renounces `role` for themselves. Emits `RoleRevoked`. +Caller renounces `role` for themselves. Emits `RoleRevoked` when membership changes. ## Parameters | Parameter | Description | |---|---| | `role` | Role to renounce. | -| `callerConfirmation` | MUST equal `msg.sender`. | +| `callerConfirmation` | Must equal `msg.sender`. | ## Reverts - `AccessControlBadConfirmation` when `callerConfirmation != msg.sender`. -- `LastAdminCannotRenounce` when `role == DEFAULT_ADMIN_ROLE` and the caller is the last default admin. +- `LastAdminCannotRenounce` when `role == DEFAULT_ADMIN_ROLE` and the caller is the last remaining `DEFAULT_ADMIN_ROLE` holder. Use `renounceLastAdmin` to clear the final admin. ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +Any account may call this function for their own roles. No privileged role is required. ## Policy Interaction @@ -43,5 +41,5 @@ No direct policy interaction. ## Example ```solidity Usage Example -IB20(target).renounceRole(role, callerConfirmation); +IB20(target).renounceRole(role, msg.sender); ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20/revoke-role.mdx b/docs/specifications/b20/reference/interfaces/ib20/revoke-role.mdx index 1f14e4741..dc969b12f 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/revoke-role.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/revoke-role.mdx @@ -1,10 +1,8 @@ --- title: "IB20.revokeRole" -description: "Generated B20 reference for revokeRole(bytes32,address)." +description: "Revokes a role from an account on a B20 token." --- - - ## Signature ```solidity IB20.sol @@ -18,7 +16,7 @@ function revokeRole(bytes32 role, address account) external; ## Description -Revokes `role` from `account`. Emits `RoleRevoked`. +Revokes `role` from `account`. The call is idempotent — if `account` does not hold `role`, the call succeeds and emits nothing. `RoleRevoked` fires only when membership actually changes. ## Parameters @@ -29,12 +27,12 @@ Revokes `role` from `account`. Emits `RoleRevoked`. ## Reverts -- `AccessControlUnauthorizedAccount` when the caller does not hold the admin role for `role`, or when the token has been transitioned to admin-less via `renounceLastAdmin`. -- `LastAdminCannotRenounce` when `role == DEFAULT_ADMIN_ROLE` and `account` is the last default admin. Use `renounceLastAdmin` to clear the final admin. +- `AccessControlUnauthorizedAccount` — caller does not hold the admin role for `role`, or the token has no admins (transitioned via `renounceLastAdmin`). +- `LastAdminCannotRenounce` — `role == DEFAULT_ADMIN_ROLE` and `account` is the last remaining `DEFAULT_ADMIN_ROLE` holder. Use `renounceLastAdmin` to clear the final admin intentionally. ## Access Control -Uses the B20 access-control role graph. Factory initCalls bypass role gates during creation. +The caller must hold `getRoleAdmin(role)`. On a fresh token every role's admin is `DEFAULT_ADMIN_ROLE`. After `setRoleAdmin` is called the new admin role applies; `revokeRole` is not hardcoded to `DEFAULT_ADMIN_ROLE`. ## Policy Interaction @@ -43,5 +41,5 @@ No direct policy interaction. ## Example ```solidity Usage Example -IB20(target).revokeRole(role, account); +IB20(target).revokeRole(MINT_ROLE, account); ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20/seize-holder-policy.mdx b/docs/specifications/b20/reference/interfaces/ib20/seize-holder-policy.mdx index ff5f2602d..b626cebc4 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/seize-holder-policy.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/seize-holder-policy.mdx @@ -1,10 +1,8 @@ --- title: "IB20.SEIZE_HOLDER_POLICY" -description: "Generated B20 reference for SEIZE_HOLDER_POLICY()." +description: "Returns the policy scope identifier consulted against the `from` address in seizeWithMemo." --- - - ## Signature ```solidity IB20.sol @@ -20,22 +18,36 @@ function SEIZE_HOLDER_POLICY() external view returns (bytes32); Policy slot consulted against `from` by `seizeWithMemo`. -A `from` is seizable only when it is NOT authorized by this policy. An unset slot reads as `0` (always-allow), so no account is seizable until an issuer configures the slot. +A `from` is seizable only when it is **not** authorized by this policy. The check is inverted relative to transfer and mint scopes: `seizeWithMemo` proceeds only when `isAuthorized(policyId, from)` returns `false`. + +When the slot is unset (`0`), the sentinel resolves to always-allow, meaning every account is authorized and therefore no account is seizable. You must attach a policy before any holder can be seized. ## Returns -Policy scope constant. +`bytes32` policy scope constant. Pass this value to `updatePolicy` and `policyId`. ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +View function. No role required. ## Policy Interaction -No direct policy interaction. +The policy ID stored at this scope is read by `seizeWithMemo` before moving any balance. The recommended policy type is `BLOCKLIST`: + +- Accounts **in** the blocklist have `isAuthorized = false` → seizable. +- Accounts **not in** the blocklist have `isAuthorized = true` → not seizable. + +An `ALLOWLIST` or the `ALWAYS_BLOCK` sentinel inverts this logic and makes every account seizable. Do not attach either to `SEIZE_HOLDER_POLICY`. ## Example ```solidity Usage Example -IB20(target).SEIZE_HOLDER_POLICY(); +// Read the scope constant +bytes32 scope = IB20(token).SEIZE_HOLDER_POLICY(); + +// Attach a blocklist policy to that scope +IB20(token).updatePolicy(scope, seizableId); + +// Read back the attached policy ID +uint64 id = IB20(token).policyId(scope); ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20/seize-receiver-policy.mdx b/docs/specifications/b20/reference/interfaces/ib20/seize-receiver-policy.mdx index c429dfaba..ca44b867c 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/seize-receiver-policy.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/seize-receiver-policy.mdx @@ -1,10 +1,8 @@ --- title: "IB20.SEIZE_RECEIVER_POLICY" -description: "Generated B20 reference for SEIZE_RECEIVER_POLICY()." +description: "Returns the policy scope key consulted against the seize destination in seizeWithMemo." --- - - ## Signature ```solidity IB20.sol @@ -18,24 +16,38 @@ function SEIZE_RECEIVER_POLICY() external view returns (bytes32); ## Description -Policy slot consulted against `to` by `seizeWithMemo`. +Returns the `bytes32` scope key that `seizeWithMemo` uses when checking whether the destination address (`to`) may receive seized tokens. -Mirrors `MINT_RECEIVER_POLICY`: always enforced on the seize destination. An unset slot reads as `0` (always-allow), so seize may send anywhere until an issuer configures the slot. +The check is a normal allow check: the call proceeds only when `isAuthorized(policyId, to)` returns `true`. When the slot is unset (`0`), the always-allow sentinel applies and any destination is permitted. ## Returns -Policy scope constant. +`bytes32` — the policy scope constant for the seize receiver slot. ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +View function. No role required. ## Policy Interaction -No direct policy interaction. +Attach a policy ID to this scope with `updatePolicy(token.SEIZE_RECEIVER_POLICY(), policyId)`. Use an `ALLOWLIST` to restrict which addresses may receive seized tokens. An unset scope (ID `0`) allows every destination. + + +`SEIZE_RECEIVER_POLICY` is independent of `TRANSFER_RECEIVER_POLICY`. Adding an address to a transfer allowlist does not make it a valid seize destination. Configure each scope separately. + ## Example -```solidity Usage Example -IB20(target).SEIZE_RECEIVER_POLICY(); +```solidity Title Restrict seize destinations to a treasury +// Read the scope key +bytes32 scope = IB20(token).SEIZE_RECEIVER_POLICY(); + +// Create an allowlist and add the treasury +uint64 destId = registry.createPolicy(policyAdmin, IPolicyRegistry.PolicyType.ALLOWLIST); +address[] memory dests = new address[](1); +dests[0] = treasury; +registry.updateAllowlist(destId, true, dests); + +// Attach the allowlist to the receiver scope +IB20(token).updatePolicy(scope, destId); ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20/seize-role.mdx b/docs/specifications/b20/reference/interfaces/ib20/seize-role.mdx index a8675607b..74209ba85 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/seize-role.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/seize-role.mdx @@ -1,10 +1,8 @@ --- title: "IB20.SEIZE_ROLE" -description: "Generated B20 reference for SEIZE_ROLE()." +description: "Returns the role required to call seizeWithMemo on a B20 token." --- - - ## Signature ```solidity IB20.sol @@ -18,22 +16,32 @@ function SEIZE_ROLE() external view returns (bytes32); ## Description -Required to call `seizeWithMemo`. +Returns the `bytes32` role constant that gates `seizeWithMemo`. Any account that does not hold this role will have its `seizeWithMemo` call revert with `AccessControlUnauthorizedAccount`. ## Returns -Role constant. +`bytes32` — the `SEIZE_ROLE` constant. ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +Read-only. Anyone may call this function to retrieve the role identifier. ## Policy Interaction -No direct policy interaction. +No direct policy interaction. The role returned here is used alongside `SEIZE_HOLDER_POLICY` and `SEIZE_RECEIVER_POLICY` to authorize a seize operation. The caller must hold `SEIZE_ROLE`, the `from` account must not be authorized under `SEIZE_HOLDER_POLICY` (i.e., an inverted blocklist check), and the `to` account must be authorized under `SEIZE_RECEIVER_POLICY`. + +## Usage -## Example +Grant this role to the account that will call `seizeWithMemo`: -```solidity Usage Example -IB20(target).SEIZE_ROLE(); +```solidity Title Grant SEIZE_ROLE and seize +bytes32 seizeRole = token.SEIZE_ROLE(); +token.grantRole(seizeRole, seizer); + +// seizer may now call: +token.seizeWithMemo(from, to, amount, memo); ``` + + +`SEIZE_ROLE` is separate from pause control. A holder of `SEIZE_ROLE` cannot seize while `PausableFeature.SEIZE` is paused — the call reverts `ContractPaused(SEIZE)`. Pausing `TRANSFER`, `MINT`, or `BURN` does not affect seize. + diff --git a/docs/specifications/b20/reference/interfaces/ib20/seize-with-memo.mdx b/docs/specifications/b20/reference/interfaces/ib20/seize-with-memo.mdx index 9310b0564..159f7d03d 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/seize-with-memo.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/seize-with-memo.mdx @@ -1,10 +1,8 @@ --- title: "IB20.seizeWithMemo" -description: "Generated B20 reference for seizeWithMemo(address,address,uint256,bytes32)." +description: "Moves tokens from a holder to a safekeeping account in a single admin call without changing totalSupply." --- - - ## Signature ```solidity IB20.sol @@ -18,39 +16,78 @@ function seizeWithMemo(address from, address to, uint256 amount, bytes32 memo) e ## Description -Seizes `amount` of `from`'s balance and reassigns it to `to` in a single admin operation. Emits, in order, `Transfer(from, to, amount)`, `Memo(caller, memo)`, and `Seized(caller, from, to, amount)`. A memo of `bytes32(0)` is permitted. +Moves `amount` from `from` to `to` in a single admin operation. `totalSupply` does not change. Emits, in order, `Transfer(from, to, amount)`, `Memo(caller, memo)`, and `Seized(caller, from, to, amount)`. A memo of `bytes32(0)` is permitted. + +This is not a burn and not a mint. After the call, `to` is an ordinary holder and can transfer, burn, or hold the tokens. + +Seize skips allowance and the transfer policies (`TRANSFER_SENDER_POLICY`, `TRANSFER_RECEIVER_POLICY`). It uses two dedicated policy scopes instead: + +| Scope | Account | Passes when | +|---|---|---| +| `SEIZE_HOLDER_POLICY` | `from` | `isAuthorized` returns `false` (inverted check) | +| `SEIZE_RECEIVER_POLICY` | `to` | `isAuthorized` returns `true` | -Admin operation: skips allowance and the transfer policies. The membership checks are that `from` is blocked under `SEIZE_HOLDER_POLICY` and `to` is authorized under `SEIZE_RECEIVER_POLICY`. -`to` is gated by `SEIZE_RECEIVER_POLICY`, which defaults to always-allow when unset, so an unconfigured token may seize to any destination (a treasury need not be allowlisted). +`SEIZE_HOLDER_POLICY` is inverted: when the scope is unset (ID `0`, always-allow), every account is authorized and therefore no account is seizable. Attach a blocklist and add the holder to make them seizable. + +`SEIZE_RECEIVER_POLICY` defaults to always-allow when unset, so an unconfigured token accepts any destination. ## Parameters -| Parameter | Description | -|---|---| -| `from` | Account whose balance is being seized. | -| `to` | Destination address for the seized balance. | -| `amount` | Amount to seize. | -| `memo` | Memo payload. | +| Parameter | Type | Description | +|---|---|---| +| `from` | `address` | Account whose balance is being seized. Must be non-zero and distinct from `to`. | +| `to` | `address` | Destination for the seized balance. Must be non-zero and distinct from `from`. | +| `amount` | `uint256` | Amount to seize. Must not exceed `from`'s balance. | +| `memo` | `bytes32` | Memo payload. `bytes32(0)` is allowed. | ## Reverts -- `ContractPaused(SEIZE)` when `SEIZE` is paused. -- `AccessControlUnauthorizedAccount` when the caller does not hold `SEIZE_ROLE`. -- `InvalidReceiver` when `to == address(0)`. -- `AccountNotSeizable` when `from` is currently authorized under `SEIZE_HOLDER_POLICY`. -- `PolicyForbids(SEIZE_RECEIVER_POLICY, ...)` when `to` is not authorized under `SEIZE_RECEIVER_POLICY`. -- `InsufficientBalance` when `from`'s balance is below `amount`. +| Error | Condition | +|---|---| +| `ContractPaused(SEIZE)` | `SEIZE` is paused. Pausing `TRANSFER`, `MINT`, or `BURN` does not block seize. | +| `AccessControlUnauthorizedAccount(caller, SEIZE_ROLE)` | Caller does not hold `SEIZE_ROLE`. | +| `InvalidReceiver(to)` | `to` is `address(0)`, or `from == to`. | +| `InvalidSender(from)` | `from` is `address(0)`. | +| `AccountNotSeizable(from)` | `from` is authorized under `SEIZE_HOLDER_POLICY` (scope unset, or holder not on the attached blocklist). | +| `PolicyForbids(SEIZE_RECEIVER_POLICY, policyId)` | `to` is not authorized under `SEIZE_RECEIVER_POLICY`. | +| `InsufficientBalance(from, balance, amount)` | `from`'s balance is below `amount`. | ## Access Control -`SEIZE_ROLE` gates seizure calls. +`SEIZE_ROLE` gates this function. The `SEIZE` pausable feature provides a second switch: a caller holding `SEIZE_ROLE` still cannot seize while `SEIZE` is paused. -## Policy Interaction +## Events -Checks `SEIZE_HOLDER_POLICY` for the holder and `SEIZE_RECEIVER_POLICY` for the destination. +Emitted on success, in this order: + +1. `Transfer(from, to, amount)` +2. `Memo(caller, memo)` +3. `Seized(caller, from, to, amount)` — topic0: `0xa9aec5d8b86e2fa2fd6ac3af62f2622e3dfdab1967d4cbbb56a5df7d74cb887c` ## Example ```solidity Usage Example -IB20(token).seizeWithMemo(holder, destination, amount, memo); +// Grant the seizer role +token.grantRole(token.SEIZE_ROLE(), seizer); + +// Create a blocklist, add the holder, attach it to the holder scope +uint64 seizableId = POLICY_REGISTRY.createPolicy(policyAdmin, IPolicyRegistry.PolicyType.BLOCKLIST); +address[] memory holders = new address[](1); +holders[0] = alice; +POLICY_REGISTRY.updateBlocklist(seizableId, true, holders); +token.updatePolicy(token.SEIZE_HOLDER_POLICY(), seizableId); + +// Optionally restrict destinations +uint64 destId = POLICY_REGISTRY.createPolicy(policyAdmin, IPolicyRegistry.PolicyType.ALLOWLIST); +address[] memory dests = new address[](1); +dests[0] = treasury; +POLICY_REGISTRY.updateAllowlist(destId, true, dests); +token.updatePolicy(token.SEIZE_RECEIVER_POLICY(), destId); + +// Execute the seize +token.seizeWithMemo(alice, treasury, amount, keccak256("court-order-123")); ``` + + +`AccountNotSeizable` selector: `0x91dbbc8d`. This error fires when the holder scope is unset or the holder is not on the attached blocklist — not when the holder is on a transfer blocklist. Transfer and seize scopes are independent. + diff --git a/docs/specifications/b20/reference/interfaces/ib20/set-role-admin.mdx b/docs/specifications/b20/reference/interfaces/ib20/set-role-admin.mdx index 8708339da..e91bafb72 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/set-role-admin.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/set-role-admin.mdx @@ -1,10 +1,8 @@ --- title: "IB20.setRoleAdmin" -description: "Generated B20 reference for setRoleAdmin(bytes32,bytes32)." +description: "Sets the admin role for a given role on a B20 token, controlling which role can grant and revoke it." --- - - ## Signature ```solidity IB20.sol @@ -18,29 +16,41 @@ function setRoleAdmin(bytes32 role, bytes32 newAdminRole) external; ## Description -Sets the admin role for `role`. Emits `RoleAdminChanged`. +Reassigns the admin of `role` to `newAdminRole`. After the call, `grantRole` and `revokeRole` for `role` require the caller to hold `newAdminRole`, not the previous admin. Only the current admin of `role` may call this function. + +Emits `RoleAdminChanged(role, previousAdminRole, newAdminRole)`. ## Parameters | Parameter | Description | |---|---| | `role` | Role whose admin is being updated. | -| `newAdminRole` | New admin role. | +| `newAdminRole` | New admin role. May be any role, including `role` itself to lock administration of that role. | ## Reverts -- `AccessControlUnauthorizedAccount` when the caller does not hold the current admin role for `role`, or when the token has been transitioned to admin-less via `renounceLastAdmin`. +- `AccessControlUnauthorizedAccount(account, neededRole)` — caller does not hold the current admin role for `role`, or the token has no admins (transitioned via `renounceLastAdmin`). ## Access Control -Uses the B20 access-control role graph. Factory initCalls bypass role gates during creation. +Caller must hold `getRoleAdmin(role)` at the time of the call. On a fresh token, every role's admin is `DEFAULT_ADMIN_ROLE`. -## Policy Interaction +Setting `newAdminRole` equal to `role` makes the role self-administrating. Combined with revoking all current holders, this permanently closes the ability to grant that role to new accounts. -No direct policy interaction. +## Events + +| Event | Emitted when | +|---|---| +| `RoleAdminChanged(role, previousAdminRole, newAdminRole)` | Admin reassignment succeeds | ## Example ```solidity Usage Example -IB20(target).setRoleAdmin(role, newAdminRole); +// Make BURN_ROLE the admin of MINT_ROLE. +// After this call, only BURN_ROLE holders can grant or revoke MINT_ROLE. +IB20(target).setRoleAdmin(MINT_ROLE, BURN_ROLE); + +// Lock MINT_ROLE permanently (self-admin, no holders). +IB20(target).revokeRole(MINT_ROLE, holder); +IB20(target).setRoleAdmin(MINT_ROLE, MINT_ROLE); ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20/unpause.mdx b/docs/specifications/b20/reference/interfaces/ib20/unpause.mdx index 65bfdba7a..7cc5c3127 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/unpause.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/unpause.mdx @@ -1,6 +1,6 @@ --- title: "IB20.unpause" -description: "Generated B20 reference for unpause(PausableFeature[])." +description: "Resumes one or more paused token operation classes on a B20 token." --- @@ -18,22 +18,33 @@ function unpause(PausableFeature[] calldata features) external; ## Description -Unpauses each of `features`. Features not listed are unaffected; duplicates are idempotent. Emits `Unpaused`. +Resumes each feature listed in `features`. Features not listed remain in their current state. Duplicates in the array are a no-op. The call does not revert when a feature is already unpaused. + +Emits `Unpaused(updater, features)` with the exact array passed. That array is not the resulting paused set — read `isPaused(feature)` or `pausedFeatures()` for the current state. ## Parameters | Parameter | Description | |---|---| -| `features` | Features to unpause. | +| `features` | One or more `PausableFeature` values to resume. Must be non-empty. | + +## PausableFeature values + +| Value | Gates | +|---|---| +| `TRANSFER` | `transfer`, `transferFrom`, and memo'd variants | +| `MINT` | `mint`, `mintWithMemo`, and Asset `batchMint` | +| `BURN` | `burn`, `burnWithMemo`, and the deprecated `burnBlocked` | +| `SEIZE` | `seizeWithMemo` | ## Reverts -- `AccessControlUnauthorizedAccount` when the caller does not hold `UNPAUSE_ROLE`. -- `EmptyFeatureSet` when `features.length == 0`. +- `AccessControlUnauthorizedAccount(account, neededRole)` — caller does not hold `UNPAUSE_ROLE`. +- `EmptyFeatureSet()` — `features` is an empty array. ## Access Control -`UNPAUSE_ROLE` gates this call. +`UNPAUSE_ROLE` gates this call. `UNPAUSE_ROLE` and `PAUSE_ROLE` are separate roles, so the account that pauses a feature does not have to be the account that resumes it. ## Policy Interaction @@ -42,5 +53,6 @@ No direct policy interaction. ## Example ```solidity Usage Example -IB20(target).unpause(features); +// Unpause BURN only; MINT remains paused if it was paused separately. +IB20(target).unpause([PausableFeature.BURN]); ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20/update-policy.mdx b/docs/specifications/b20/reference/interfaces/ib20/update-policy.mdx index 6fe519d85..f2872fbfc 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/update-policy.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/update-policy.mdx @@ -1,10 +1,8 @@ --- title: "IB20.updatePolicy" -description: "Generated B20 reference for updatePolicy(bytes32,uint64)." +description: "Binds a Policy Registry policy ID to a policy scope on a B20 token." --- - - ## Signature ```solidity IB20.sol @@ -18,31 +16,48 @@ function updatePolicy(bytes32 policyScope, uint64 newPolicyId) external; ## Description -Updates the policy ID assigned to `policyScope`. Takes effect immediately for the next operation that consults this slot. Emits `PolicyUpdated`. +Binds `newPolicyId` to `policyScope`. Takes effect immediately: the next operation that consults that scope calls `isAuthorized(newPolicyId, account)` on the Policy Registry. Emits `PolicyUpdated`. + +Until a scope is updated it holds `0` (`ALWAYS_ALLOW`), so the check passes for every address. ## Parameters | Parameter | Description | |---|---| -| `policyScope` | Policy slot scope. | -| `newPolicyId` | Policy ID to assign to the slot. | +| `policyScope` | The scope slot to update. Must be a scope this token recognizes. | +| `newPolicyId` | The policy ID to assign. Must be a built-in sentinel (`ALWAYS_ALLOW`, `ALWAYS_BLOCK`) or an existing registry policy. | ## Reverts -- `AccessControlUnauthorizedAccount` when the caller does not hold `DEFAULT_ADMIN_ROLE`. -- `UnsupportedPolicyType` when `policyScope` is not recognized by this token. -- `PolicyNotFound` when `newPolicyId` is not a built-in sentinel and does not exist in the registry. +| Error | Condition | +|---|---| +| `AccessControlUnauthorizedAccount` | Caller does not hold `DEFAULT_ADMIN_ROLE`. | +| `UnsupportedPolicyType(policyScope)` | `policyScope` is not a slot this token supports. | +| `PolicyNotFound(newPolicyId)` | `newPolicyId` is not a built-in sentinel and does not exist in the registry. | ## Access Control -`DEFAULT_ADMIN_ROLE` gates this call. Factory initCalls bypass the role gate during creation. +`DEFAULT_ADMIN_ROLE` gates this call. During token creation, `initCalls` may bind policies in the same transaction; those calls bypass the role gate. -## Policy Interaction +## Policy Scopes -Reads or writes a token policy-scope pointer into the PolicyRegistry. +The recognized scopes and the accounts they check are: + +| Scope | Checked account | Denies when `isAuthorized` is | +|---|---|---| +| `TRANSFER_SENDER_POLICY` | `from` | `false` | +| `TRANSFER_RECEIVER_POLICY` | `to` | `false` | +| `TRANSFER_EXECUTOR_POLICY` | `msg.sender` (on `transferFrom` when `msg.sender != from`) | `false` | +| `MINT_RECEIVER_POLICY` | `to` | `false` | +| `SEIZE_HOLDER_POLICY` | `from` | `true` | +| `SEIZE_RECEIVER_POLICY` | `to` | `false` | + +Transfer scopes are skipped on factory `initCalls` transfers. `MINT_RECEIVER_POLICY` is always checked, including factory `initCalls` mints. `SEIZE_HOLDER_POLICY` unset (`ALWAYS_ALLOW`) means no account is seizable. ## Example ```solidity Usage Example -IB20(token).updatePolicy(B20Constants.MINT_RECEIVER_POLICY, policyId); +IB20(token).updatePolicy(B20Constants.TRANSFER_RECEIVER_POLICY, policyId); ``` + +The same policy ID can be bound to more than one scope and to more than one token. Updating membership in the registry policy is visible immediately on every scope and token that references it — no second `updatePolicy` call is needed. From e405074e7f35dca77a4e00067fa449647a1fd9d0 Mon Sep 17 00:00:00 2001 From: Soheima M Date: Tue, 8 Sep 2026 18:30:19 +0200 Subject: [PATCH 2/4] docs(b20): fix style, restore verified samples, rename seize scope, fact-check sync Follow-up to the be6d045 sync (#1939). CI - Six code fences over seven lines gain lines/expandable; one fence gains a language. - The three stock-split verified samples are back on the multiplier guide and now show the scheduled updateUIMultiplier flow. Fixtures, the TypeScript ABI, and the live script are updated to match; the Solidity samples pin base-std@be6d045 because neither Beryl tag has the Cobalt surface. Style - Guide titles return to the guideline names (Announce a Distribution, Apply a Multiplier). Headings are title case, code fences use Title or filename meta, em dashes are gone, "on-chain" is "onchain". - Touched reference pages follow the master section order (Signature, Description, Parameters, Returns, Reverts, Events, Access Control, Policy Interaction, Example); ad-hoc Behavior/Notes/Timing/Related sections fold into Description. SEIZE_EXEMPT_POLICY - base-std#214 renamed SEIZE_HOLDER_POLICY; its sync never landed. The reference page is renamed with a redirect, and every mention, the selector (0xfeb346ec), and the hash are updated across the spec, constants, guides, and the Cobalt changelog entries. Fact-check against the interfaces at be6d045 - isPaused/pause/unpause selectors now hash the uint8 ABI encoding. - effectiveAt does not reset to 0 at maturity; only cancel resets it. - multiplier() and scaledBalanceOf are aliases, not deprecated. - createCompositePolicy also emits CompositePolicyUpdated; InvalidChildPolicy covers built-in sentinels; PolicyScope.* becomes B20Constants.*. - Stablecoin currency codes are self-declared, not ISO-validated; batchMint is Asset-only; the batch cap has no MAX_BATCH_SIZE constant. - Ungrounded claims removed: pause bitmask storage layout, renounceAdmin clearing the pending admin, ULP bounds on the raw side, storage-free maturation. Role idempotence and the instant-setter event order are attributed to OpenZeppelin and the base-std guide respectively. - Invalid Solidity array literals in examples replaced with memory arrays. --- .../b20/changelog/02-cobalt-b20-seize.mdx | 20 ++-- ...cobalt-policyregistry-composite-policy.mdx | 2 +- .../issue-rwa/announce-a-distribution.mdx | 102 +++++++++--------- .../issue-rwa/apply-a-multiplier.mdx | 84 +++++++-------- .../issue-rwa/cancel-blocked-units.mdx | 2 +- .../issue-rwa/create-an-asset-token.mdx | 2 +- .../issue-rwa/pause-transfers.mdx | 4 +- .../issue-your-stablecoin.mdx | 6 +- .../issue-stablecoins/pause-activity.mdx | 6 +- .../restrict-who-can-hold.mdx | 4 +- docs/docs.json | 6 +- .../b20/reference/constants-addresses.mdx | 2 +- .../create-composite-policy.mdx | 42 ++++---- .../finalize-update-admin.mdx | 22 ++-- .../pending-policy-admin.mdx | 14 +-- .../i-policy-registry/policy-admin.mdx | 18 ++-- .../i-policy-registry/renounce-admin.mdx | 13 +-- .../i-policy-registry/stage-update-admin.mdx | 26 ++--- .../i-policy-registry/update-allowlist.mdx | 23 ++-- .../i-policy-registry/update-blocklist.mdx | 43 ++++---- .../i-policy-registry/update-composite.mdx | 14 +-- .../interfaces/ib20-asset/announce.mdx | 24 ++--- .../interfaces/ib20-asset/batch-mint.mdx | 20 ++-- .../interfaces/ib20-asset/effective-at.mdx | 15 +-- .../interfaces/ib20-asset/multiplier.mdx | 12 +-- .../interfaces/ib20-asset/operator-role.mdx | 8 +- .../ib20-asset/scaled-balance-of.mdx | 24 ++--- .../ib20-asset/to-scaled-balance.mdx | 11 +- .../interfaces/ib20-asset/to-ui-amount.mdx | 16 ++- .../interfaces/ib20-asset/ui-multiplier.mdx | 16 +-- .../ib20-asset/update-multiplier.mdx | 14 +-- .../interfaces/ib20-factory/create-b20.mdx | 24 ++--- .../ib20-factory/is-b20-initialized.mdx | 10 +- .../interfaces/ib20-factory/is-b20.mdx | 4 +- .../interfaces/ib20/burn-blocked.mdx | 2 +- .../reference/interfaces/ib20/grant-role.mdx | 2 +- .../b20/reference/interfaces/ib20/index.mdx | 8 +- .../reference/interfaces/ib20/is-paused.mdx | 18 ++-- .../b20/reference/interfaces/ib20/pause.mdx | 33 +++--- .../interfaces/ib20/paused-features.mdx | 14 ++- .../reference/interfaces/ib20/policy-id.mdx | 6 +- .../reference/interfaces/ib20/revoke-role.mdx | 6 +- .../interfaces/ib20/seize-exempt-policy.mdx | 55 ++++++++++ .../interfaces/ib20/seize-holder-policy.mdx | 53 --------- .../interfaces/ib20/seize-receiver-policy.mdx | 4 +- .../reference/interfaces/ib20/seize-role.mdx | 8 +- .../interfaces/ib20/seize-with-memo.mdx | 22 ++-- .../interfaces/ib20/set-role-admin.mdx | 14 +-- .../b20/reference/interfaces/ib20/unpause.mdx | 34 +++--- .../interfaces/ib20/update-policy.mdx | 6 +- .../b20/specification-overview.mdx | 4 +- .../cli/b20-operations.sh | 5 +- .../solidity/script/B20Examples.s.sol | 9 +- .../typescript/src/b20/abi.ts | 6 ++ .../src/b20/stock/stock-split-ts.ts | 12 +-- .../typescript/src/live/b20.ts | 4 +- scripts/sync-from-base-std/route-table.json | 2 +- scripts/verify-doc-samples.sh | 4 +- 58 files changed, 464 insertions(+), 520 deletions(-) create mode 100644 docs/specifications/b20/reference/interfaces/ib20/seize-exempt-policy.mdx delete mode 100644 docs/specifications/b20/reference/interfaces/ib20/seize-holder-policy.mdx diff --git a/docs/base-chain/specs/reference/b20/changelog/02-cobalt-b20-seize.mdx b/docs/base-chain/specs/reference/b20/changelog/02-cobalt-b20-seize.mdx index 521116f4f..4c632817f 100644 --- a/docs/base-chain/specs/reference/b20/changelog/02-cobalt-b20-seize.mdx +++ b/docs/base-chain/specs/reference/b20/changelog/02-cobalt-b20-seize.mdx @@ -12,7 +12,7 @@ description: "The B20 seize surface at Cobalt and the deprecation of burnBlocked At Cobalt, the base B20 surface gains a first-class seize operation. `seizeWithMemo(from, to, amount, memo)` reassigns a holder's balance to a destination in one admin call, gated by a new -`SEIZE_ROLE`, a new `SEIZE` pause vector, and two new policy slots (`SEIZE_HOLDER_POLICY`, +`SEIZE_ROLE`, a new `SEIZE` pause vector, and two new policy slots (`SEIZE_EXEMPT_POLICY`, `SEIZE_RECEIVER_POLICY`). Nothing you call today breaks: every Beryl selector, event topic, and error keeps its exact 4-byte @@ -23,7 +23,7 @@ To migrate, move administrative balance removal from `burnBlocked` to `seizeWith treasury or self address, then call `burn` if you want the supply destroyed. Seize is opt-in per token. The surface exists at Cobalt, but seize does nothing until the issuer -configures `SEIZE_HOLDER_POLICY`. With the slot unset (always-allow), no account is seizable, and +configures `SEIZE_EXEMPT_POLICY`. With the slot unset (always-allow), no account is seizable, and every `seizeWithMemo` call reverts `AccountNotSeizable`. An issuer that never sets the policy has, in effect, no seize capability on that token. @@ -45,7 +45,7 @@ at Cobalt. Seize lives on the shared `IB20` surface, so it's identical across As | `BURN_BLOCKED_ROLE()` `0x32ad9be8` | `BURN_BLOCKED_ROLE()` `0x32ad9be8` | carried over unchanged | Still gates `burnBlocked` only. | | — | `seizeWithMemo(address,address,uint256,bytes32)` `0xf916d81b` | new | Admin balance reassignment. A transfer, not a burn. | | — | `SEIZE_ROLE()` `0x3c7e9ba5` | new | Required to call `seizeWithMemo`. Value `keccak256("SEIZE_ROLE")` = `0x3469b8b0d89e9604f8510ed143f74a8336d22955d4f83e23bf53d9414e27f432`. | -| — | `SEIZE_HOLDER_POLICY()` `0xb279d311` | new | Policy slot checked against `from`. Value `keccak256("SEIZE_HOLDER_POLICY")` = `0x1497ab2b67ebb0a75dd9cdd6aec9f0e64620e6b87e911af7a088ac12e58d9ef2`. | +| — | `SEIZE_EXEMPT_POLICY()` `0xfeb346ec` | new | Policy slot checked against `from`. Value `keccak256("SEIZE_EXEMPT_POLICY")` = `0xedb5da348cfb67af08746d3afd1be81034b50d5c8576f31aff688f39dfd540ed`. | | — | `SEIZE_RECEIVER_POLICY()` `0xb31da27f` | new | Policy slot checked against `to`. Value `keccak256("SEIZE_RECEIVER_POLICY")` = `0xbf15b19caf5c77422c038bc25f26b8b815c3a14f6d04c6616076b81bcfe07b3d`. | ### Events @@ -60,7 +60,7 @@ at Cobalt. Seize lives on the shared `IB20` surface, so it's identical across As | Beryl error (selector) | Cobalt (selector) | Status | Why | | --- | --- | --- | --- | | `AccountNotBlocked(address)` `0x64a5cb46` | unchanged | present on Beryl already | Thrown by `burnBlocked` when `from` is authorized under `TRANSFER_SENDER_POLICY` (that is, not blocked). | -| — | `AccountNotSeizable(address)` `0x91dbbc8d` | new | Thrown by `seizeWithMemo` when `from` is authorized under `SEIZE_HOLDER_POLICY` (that is, not seizable). | +| — | `AccountNotSeizable(address)` `0x91dbbc8d` | new | Thrown by `seizeWithMemo` when `from` is authorized under `SEIZE_EXEMPT_POLICY` (that is, not seizable). | ### Pause Features @@ -90,9 +90,9 @@ Requirements and guards: - **Pause**: `SEIZE` must not be paused, or the call reverts `ContractPaused(SEIZE)`. - **Addresses**: `to != address(0)` and `from != to`, or the call reverts `InvalidReceiver`. `from != address(0)`, or the call reverts `InvalidSender`. -- **Holder gate**: `from` must be blocked under `SEIZE_HOLDER_POLICY`, that is, not authorized by +- **Holder gate**: `from` must be blocked under `SEIZE_EXEMPT_POLICY`, that is, not authorized by it, or the call reverts `AccountNotSeizable`. An unset slot reads as always-allow, so no account - is seizable until an issuer configures `SEIZE_HOLDER_POLICY`. + is seizable until an issuer configures `SEIZE_EXEMPT_POLICY`. - **Destination gate**: `to` must be authorized under `SEIZE_RECEIVER_POLICY`, which mirrors `MINT_RECEIVER_POLICY` and is always enforced. But an unset slot is always-allow, so a token can seize to any destination (a treasury doesn't need to be allowlisted) until the slot is set. @@ -124,7 +124,7 @@ untouched. `burnBlocked` is the burn: it sends to `address(0)` and reduces suppl old burn-blocked outcome, seize to a treasury or self address, then call `burn`. **Q: `seizeWithMemo` and `burnBlocked` both target "bad" accounts. Do they read the same set?** -No, and this is deliberate. `seizeWithMemo` reads `SEIZE_HOLDER_POLICY`. `burnBlocked` reads +No, and this is deliberate. `seizeWithMemo` reads `SEIZE_EXEMPT_POLICY`. `burnBlocked` reads `TRANSFER_SENDER_POLICY`. A token can define a seizable set that's distinct from its transfer-blocked set. In both cases, "eligible" means not authorized by the relevant policy, and an unset policy (always-allow) means nobody is eligible. @@ -138,14 +138,14 @@ No. `seizeWithMemo` requires `SEIZE_ROLE`. `burnBlocked` requires `BURN_BLOCKED_ doesn't grant the other. **Q: I never configured the seize policies. What happens if I call `seizeWithMemo`?** -It reverts `AccountNotSeizable(from)` for every `from`, because an unset `SEIZE_HOLDER_POLICY` is -always-allow, so no account is seizable. You must configure `SEIZE_HOLDER_POLICY` to designate +It reverts `AccountNotSeizable(from)` for every `from`, because an unset `SEIZE_EXEMPT_POLICY` is +always-allow, so no account is seizable. You must configure `SEIZE_EXEMPT_POLICY` to designate seizable holders before seize does anything. (Leaving `SEIZE_RECEIVER_POLICY` unset simply permits any destination.) **Q: Does seize consult the transfer policies or spend an allowance?** No. It's an admin operation: it bypasses `TRANSFER_SENDER/RECEIVER/EXECUTOR_POLICY` and allowances, -and enforces only `SEIZE_HOLDER_POLICY` (on `from`) and `SEIZE_RECEIVER_POLICY` (on `to`). +and enforces only `SEIZE_EXEMPT_POLICY` (on `from`) and `SEIZE_RECEIVER_POLICY` (on `to`). **Q: Is seize available on B20 Stablecoin as well as B20 Asset?** Yes. It's defined on the shared `IB20` surface, so both variants expose the identical diff --git a/docs/base-chain/specs/reference/b20/changelog/02-cobalt-policyregistry-composite-policy.mdx b/docs/base-chain/specs/reference/b20/changelog/02-cobalt-policyregistry-composite-policy.mdx index 231d7f427..0390e5c64 100644 --- a/docs/base-chain/specs/reference/b20/changelog/02-cobalt-policyregistry-composite-policy.mdx +++ b/docs/base-chain/specs/reference/b20/changelog/02-cobalt-policyregistry-composite-policy.mdx @@ -139,7 +139,7 @@ that gates `createPolicy`, `updateAllowlist`, and others. There's no composite-s flag. `compositePolicyChildIds`, `MIN_COMPOSITE_CHILD_POLICIES`, `MAX_COMPOSITE_CHILD_POLICIES`, and `isAuthorized` on a composite ID are all always callable, whether or not the feature is active. -**Q: Can a B20 token's policy slot (for example, `TRANSFER_SENDER_POLICY` or `SEIZE_HOLDER_POLICY`) +**Q: Can a B20 token's policy slot (for example, `TRANSFER_SENDER_POLICY` or `SEIZE_EXEMPT_POLICY`) reference a composite ID?** Yes. B20 stores every policy slot as an opaque `uint64 policyId` and calls `isAuthorized`, so a composite ID works exactly like a simple one, and no B20-side change was needed. As with any policy diff --git a/docs/build-on-base/issue-rwa/announce-a-distribution.mdx b/docs/build-on-base/issue-rwa/announce-a-distribution.mdx index cfbd031c3..b9fe8d3d9 100644 --- a/docs/build-on-base/issue-rwa/announce-a-distribution.mdx +++ b/docs/build-on-base/issue-rwa/announce-a-distribution.mdx @@ -1,7 +1,7 @@ --- -title: "Announce a Corporate Action" +title: "Announce a Distribution" description: "Wrap a holder-impacting action with an onchain disclosure using the B20 Asset announce function." -keywords: ["announce corporate action", "B20 announcement", "asset distribution", "batchMint", "stock split", "treasury burn"] +keywords: ["announce distribution", "B20 announcement", "asset distribution", "batchMint distribution", "stock split", "treasury burn"] --- import { AssetDemo } from "/snippets/AssetDemo.jsx" @@ -20,28 +20,22 @@ The demo uses a local browser-generated account to submit real transactions on * -New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. +New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. -## How `announce` works +## How Announcements Work -`announce` takes `internalCalls`, a single-use `id`, a `description`, and an optional `uri`. +`announce` takes `internalCalls`, a single-use `id`, a `description`, and a `uri`. -- **`internalCalls`** — ABI-encoded calldata blobs targeting this asset. Each blob must be at least 4 bytes. Pass an empty array for a notice with no on-chain effect. -- **`id`** — chosen by the caller; consumed on success. Reuse reverts `AnnouncementIdAlreadyUsed`. After success, `isAnnouncementIdUsed(id)` returns `true`. -- **`description` / `uri`** — operator-supplied strings. The asset does not verify them. +- **`internalCalls`**: ABI-encoded calldata blobs targeting this asset. Each blob must be at least 4 bytes. Pass an empty array for a notice with no onchain effect. +- **`id`**: chosen by the caller and consumed on success. Reuse reverts `AnnouncementIdAlreadyUsed`. After success, `isAnnouncementIdUsed(id)` returns `true`. +- **`description` and `uri`**: operator-supplied strings. The asset does not verify them. -Execution order: +The asset emits `Announcement(caller, id, description, uri)`, runs the inner calls atomically, then emits `EndAnnouncement(id)`. If any inner call fails, the whole transaction reverts. Nesting `announce` inside an inner call reverts `AnnouncementInProgress`. -1. Emit `Announcement(caller, id, description, uri)`. -2. Run the inner calls atomically. -3. Emit `EndAnnouncement(id)`. +The caller must hold `OPERATOR_ROLE`. Any other caller reverts `AccessControlUnauthorizedAccount`. Inner calls keep their own role gates. If an inner call needs `MINT_ROLE` or `BURN_ROLE`, grant those to the operator as well. -If any inner call fails, the whole transaction reverts and `id` is not consumed. Nesting `announce` inside an inner call reverts `AnnouncementInProgress`. - -The caller must hold `OPERATOR_ROLE`. Any other caller reverts `AccessControlUnauthorizedAccount`. Inner calls keep their own role gates — if an inner call needs `MINT_ROLE` or `BURN_ROLE`, grant those to the operator as well. - -## Scenario 1 — Stock dividend with additional shares +## Announce and Distribute the Stock Dividend {/* sample: stock-dividend-ts */} @@ -84,61 +78,69 @@ export async function announceStockDividend(token: Address, holders: Address[]) ``` +The operator needs `OPERATOR_ROLE` and `MINT_ROLE`. Recipients must pass `MINT_RECEIVER_POLICY`. `MINT` must not be paused. On success the asset emits `Announcement`, then one `Transfer(address(0), recipient, amount)` per recipient, then `EndAnnouncement`. + + +On success, the unique announcement `id` is marked used and the batch mint executes between `Announcement` and `EndAnnouncement`. + + -This scenario issues additional shares. A reinvested dividend that only rescales the UI belongs in Scenario 2. +This example issues additional shares. It does not distribute a cash dividend. A reinvested dividend that only rescales displayed balances is a multiplier update, covered below. -The operator needs `OPERATOR_ROLE` and `MINT_ROLE`. Recipients must pass `MINT_RECEIVER_POLICY`. `MINT` must not be paused. On success the asset emits `Announcement`, then one `Transfer(address(0), recipient, amount)` per recipient, then `EndAnnouncement`. +## Wrap Other Corporate Actions -## Scenario 2 — Stock split or reverse split +The same bracket discloses any operator-driven change. Each scenario below lists the roles the operator needs and the events the asset emits. -Wrap `updateUIMultiplier(newMultiplier, effectiveAt)`. A 2-for-1 split uses `2e18`; a reverse split uses a value below `1e18`. The operator needs `OPERATOR_ROLE` only. +### Stock Split or Reverse Split -```solidity Solidity lines wrap expandable - function announceSplit(address token, uint256 newMultiplier, uint256 effectiveAt) public { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeCall(IB20Asset.updateUIMultiplier, (newMultiplier, effectiveAt)); - IB20Asset(token).announce( - calls, - "2026-split-01", - "2-for-1 stock split", - "https://example.com/corporate-actions/2026-split-01" - ); - } +Wrap `updateUIMultiplier(newMultiplier, effectiveAt)`. A 2-for-1 split uses `2e18`. A reverse split uses a value below `1e18`. The operator needs `OPERATOR_ROLE` only. + +```solidity Title Announce a scheduled split lines wrap expandable highlight={3} +function announceSplit(address token, uint256 newMultiplier, uint256 effectiveAt) public { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeCall(IB20Asset.updateUIMultiplier, (newMultiplier, effectiveAt)); + IB20Asset(token).announce( + calls, + "2026-split-01", + "2-for-1 stock split", + "https://example.com/corporate-actions/2026-split-01" + ); +} ``` -On success the asset emits `Announcement`, `UIMultiplierUpdated`, then `EndAnnouncement`. `UIMultiplierUpdated` means the schedule was recorded, not that the multiplier is already active. +On success the asset emits `Announcement`, `UIMultiplierUpdated`, then `EndAnnouncement`. `UIMultiplierUpdated` means the schedule was recorded, not that the multiplier is already active. See [Apply a Multiplier](/build-on-base/issue-rwa/apply-a-multiplier) for the full schedule, cancel, and override flow. -To replace a live pending update, cancel first then reschedule in one `announce`: +To replace a live pending update, cancel and reschedule in one `announce`: -```solidity Solidity lines wrap expandable - bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeCall(IB20Asset.cancelUIMultiplierUpdate, ()); - calls[1] = abi.encodeCall(IB20Asset.updateUIMultiplier, (newMultiplier, effectiveAt)); - IB20Asset(token).announce(calls, id, description, uri); +```solidity Title Cancel and reschedule in one announcement wrap +bytes[] memory calls = new bytes[](2); +calls[0] = abi.encodeCall(IB20Asset.cancelUIMultiplierUpdate, ()); +calls[1] = abi.encodeCall(IB20Asset.updateUIMultiplier, (newMultiplier, effectiveAt)); +IB20Asset(token).announce(calls, id, description, uri); ``` -## Scenario 3 — Treasury burn +### Treasury Burn Wrap `burnWithMemo(amount, memo)`. The call burns the operator's own balance. The operator needs `OPERATOR_ROLE` and `BURN_ROLE`. `BURN` must not be paused. -```solidity Solidity lines wrap expandable - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeCall(IB20Asset.burnWithMemo, (amount, memo)); - IB20Asset(token).announce(calls, id, "Treasury burn", uri); +```solidity Title Announce a treasury burn wrap +bytes[] memory calls = new bytes[](1); +calls[0] = abi.encodeCall(IB20Asset.burnWithMemo, (amount, memo)); +IB20Asset(token).announce(calls, id, "Treasury burn", uri); ``` On success the asset emits `Announcement`, `Transfer(operator, address(0), amount)`, `Memo`, then `EndAnnouncement`. `totalSupply` decreases. -## Scenario 4 — Notice with no on-chain effect +### Notice With No Onchain Effect Pass an empty `internalCalls` array. The operator needs `OPERATOR_ROLE` only. The asset emits `Announcement` then `EndAnnouncement` with nothing between them. The `id` is still consumed. -```solidity Solidity Title="Notice only" - IB20Asset(token).announce(new bytes[](0), id, description, uri); +```solidity Title Notice only +IB20Asset(token).announce(new bytes[](0), id, description, uri); ``` -## Common errors +## Common Errors | Error | Cause | Fix | | --- | --- | --- | @@ -150,10 +152,6 @@ Pass an empty `internalCalls` array. The operator needs `OPERATOR_ROLE` only. Th Typical inner causes of `InternalCallFailed`: missing `MINT_ROLE` or `BURN_ROLE`, paused `MINT` or `BURN`, `UIMultiplierUpdateExists`, `PolicyForbids`, `SupplyCapExceeded`, `InsufficientBalance`. A Solidity `Panic` (for example overflow) propagates raw and is not wrapped as `InternalCallFailed`. - -On success, the unique announcement `id` is marked used and inner events sit between `Announcement` and `EndAnnouncement`. - - ## See Also diff --git a/docs/build-on-base/issue-rwa/apply-a-multiplier.mdx b/docs/build-on-base/issue-rwa/apply-a-multiplier.mdx index dc6fc0eb5..1156970d6 100644 --- a/docs/build-on-base/issue-rwa/apply-a-multiplier.mdx +++ b/docs/build-on-base/issue-rwa/apply-a-multiplier.mdx @@ -1,18 +1,18 @@ --- -title: "Schedule a Stock Split" -description: "Schedule a B20 Asset multiplier update so displayed share counts change at a future time without rewriting raw balances." +title: "Apply a Multiplier" +description: "Schedule a B20 Asset multiplier update so displayed share counts change at a future time without rewriting raw balances. A stock split is one example." keywords: ["asset token multiplier", "B20 multiplier", "scaled token balance", "stock split", "updateUIMultiplier"] --- import { AssetDemo } from "/snippets/AssetDemo.jsx" import RwaDisclaimer from "/snippets/RwaDisclaimer.mdx" -Schedule a 2-for-1 stock split by calling `updateUIMultiplier(2e18, effectiveAt)` on a B20 Asset. Raw `balanceOf`, `totalSupply`, and transfer amounts stay unchanged. Wallets and indexers read the post-split share count through `balanceOfUI` and related views after `effectiveAt` passes. +Schedule a 2-for-1 stock split by calling `updateUIMultiplier(2e18, effectiveAt)` on a B20 Asset. Raw `balanceOf`, `totalSupply`, and transfer amounts stay unchanged. Wallets and indexers read the post-split share count through `balanceOfUI` and related views once `effectiveAt` passes. -The routine path is `updateUIMultiplier`, not the deprecated `updateMultiplier`. `updateMultiplier` applies a value immediately and clears any pending update — use it only as an emergency override. +The routine path is `updateUIMultiplier`. The deprecated `updateMultiplier` applies a value immediately and clears any pending update. Use it only as an emergency override. ## Demo @@ -24,7 +24,7 @@ The demo uses a local browser-generated account to submit real transactions on * -New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@v1.0.0`, `viem@2.55.11`, and Base Foundry `v1.1.1`. +New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## How the Multiplier Works @@ -38,51 +38,39 @@ Raw `balanceOf` is unchanged by a split. The multiplier changes only the derived | `totalSupplyUI()` | `totalSupply() * uiMultiplier() / 1e18` | | `toUIAmount(raw)` / `fromUIAmount(ui)` | Convert at the effective multiplier | -The multiplier is an 18-decimal WAD: `1e18` is `1.0`. A 2-for-1 split uses `2e18`. A 1-for-2 reverse split uses `5e17`. Integer division rounds down, so a round trip through `toUIAmount` and `fromUIAmount` can lose up to one unit in the last place when `multiplier != 1e18`. Prefer 18 decimals for equities to keep that effect small. +The multiplier is an 18-decimal WAD: `1e18` is `1.0`. A 2-for-1 split uses `2e18`. A 1-for-2 reverse split uses `5e17`. Integer division rounds down, so the rounding loss is confined to the scaled view and is at most one unit of the scaled amount. The raw-side difference on a round trip through `fromUIAmount` can be larger when the multiplier is below `1e18`. Prefer 18 decimals for stock tokens to keep that effect small. ## Schedule and Verify the Split Multiplier -### Prerequisites - -- A B20 Asset you administer. -- `DEFAULT_ADMIN_ROLE` to grant `OPERATOR_ROLE`. -- A future `effectiveAt` timestamp and a `newMultiplier` in `(0, MAX_UI_MULTIPLIER]`. - -Only an account holding `OPERATOR_ROLE` may call `updateUIMultiplier`, `cancelUIMultiplierUpdate`, or `updateMultiplier`. Any other caller reverts `AccessControlUnauthorizedAccount`. - -### Grant `OPERATOR_ROLE` and schedule +Only an account holding `OPERATOR_ROLE` may call `updateUIMultiplier`, `cancelUIMultiplierUpdate`, or `updateMultiplier`. Any other caller reverts `AccessControlUnauthorizedAccount`. The token created in [Create an Asset Token](/build-on-base/issue-rwa/create-an-asset-token) grants `OPERATOR_ROLE` to the deployer. -```typescript TypeScript lines wrap expandable highlight={7,8} +{/* sample: stock-split-ts */} +```typescript TypeScript lines wrap expandable highlight={8} import { type Address } from "viem"; import { publicClient } from "../../shared/clients.js"; import { assetAbi } from "../abi.js"; import { sendContract } from "../write.js"; -export async function scheduleTwoForOneSplit(token: Address, operator: Address) { - await sendContract({ address: token, abi: assetAbi, functionName: "grantRole", - args: [await publicClient.readContract({ address: token, abi: assetAbi, functionName: "OPERATOR_ROLE" }), operator] }); - const effectiveAt = BigInt(Math.floor(Date.now() / 1000) + 86400); // 1 day from now - await sendContract({ address: token, abi: assetAbi, functionName: "updateUIMultiplier", - args: [2n * 10n ** 18n, effectiveAt] }); - const newMultiplier = await publicClient.readContract({ address: token, abi: assetAbi, functionName: "newUIMultiplier" }); - if (newMultiplier !== 2n * 10n ** 18n) throw new Error("Split not scheduled"); +export async function scheduleTwoForOneSplit(token: Address) { + const effectiveAt = BigInt(Math.floor(Date.now() / 1000) + 86_400); + await sendContract({ address: token, abi: assetAbi, functionName: "updateUIMultiplier", args: [2n * 10n ** 18n, effectiveAt] }); + const pending = await publicClient.readContract({ address: token, abi: assetAbi, functionName: "newUIMultiplier" }); + if (pending !== 2n * 10n ** 18n) throw new Error("Split was not scheduled"); return effectiveAt; } ``` -```solidity Solidity lines wrap expandable highlight={3,4} -function scheduleAndVerifySplit(address token, address operator) public returns (uint256 scheduled) { - IB20Asset(token).grantRole(IB20Asset(token).OPERATOR_ROLE(), operator); - uint256 effectiveAt = block.timestamp + 1 days; +{/* sample: stock-split-solidity */} +```solidity Solidity lines wrap expandable highlight={3} +function scheduleSplit(address token) public returns (uint256 effectiveAt) { + effectiveAt = block.timestamp + 1 days; IB20Asset(token).updateUIMultiplier(2e18, effectiveAt); - // While pending: uiMultiplier() is still 1e18, newUIMultiplier() is 2e18 require(IB20Asset(token).newUIMultiplier() == 2e18, "split not scheduled"); - return effectiveAt; + require(IB20Asset(token).effectiveAt() == effectiveAt, "effectiveAt mismatch"); } ``` +{/* sample: stock-split-cli */} ```bash CLI lines wrap expandable highlight={2} -base-cast send "$TOKEN_ADDRESS" "grantRole(bytes32,address)" "$OPERATOR_ROLE" "$OPERATOR" \ - --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" EFFECTIVE_AT=$(( $(date +%s) + 86400 )) base-cast send "$TOKEN_ADDRESS" "updateUIMultiplier(uint256,uint256)" 2000000000000000000 "$EFFECTIVE_AT" \ --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" @@ -90,21 +78,21 @@ base-cast call "$TOKEN_ADDRESS" "newUIMultiplier()(uint256)" --rpc-url "$RPC_URL ``` -On success, the asset emits `UIMultiplierUpdated(oldMultiplier, newMultiplier, effectiveAtTimestamp)`. That event fires when the update is **recorded**, not when the multiplier becomes active. +On success, the asset emits `UIMultiplierUpdated(oldMultiplier, newMultiplier, effectiveAtTimestamp)`. That event fires when the update is recorded, not when the multiplier becomes active. -### Read the live pending state +### Read the Live Pending State -```solidity Solidity -asset.uiMultiplier(); // still the old (current) multiplier +```solidity Title Read the pending state +asset.uiMultiplier(); // still the current multiplier asset.newUIMultiplier(); // scheduled target asset.effectiveAt(); // flip timestamp ``` A second `updateUIMultiplier` while a pending update is live reverts `UIMultiplierUpdateExists`. To replace a pending update, cancel first, then reschedule. -### Confirm after `effectiveAt` +### Confirm After the Effective Time -When `block.timestamp >= effectiveAt`, `uiMultiplier()` returns the new multiplier. Maturation does **not** emit an event and does **not** write storage. Do not wait for a second event at the flip. +When `block.timestamp >= effectiveAt`, `uiMultiplier()` returns the new multiplier. Maturation emits no event. Do not wait for a second event at the flip. `uiMultiplier()` returns `2e18` at or after `effectiveAt`. Raw `balanceOf` stays unchanged. `balanceOfUI` doubles. @@ -114,29 +102,29 @@ When `block.timestamp >= effectiveAt`, `uiMultiplier()` returns the new multipli Call before `effectiveAt` to discard the pending split: -```solidity Solidity +```solidity Title Cancel the pending update asset.cancelUIMultiplierUpdate(); ``` -Emits `UIMultiplierUpdateCancelled(cancelledMultiplier, cancelledEffectiveAt)`. Calling with no live pending (including after maturity) reverts `UIMultiplierUpdateDoesNotExist`. +Emits `UIMultiplierUpdateCancelled(cancelledMultiplier, cancelledEffectiveAt)`. Calling with no live pending update, including after maturity, reverts `UIMultiplierUpdateDoesNotExist`. To cancel and reschedule atomically, wrap both calls in `announce`: -```solidity Solidity lines wrap +```solidity Title Cancel and reschedule in one announcement wrap bytes[] memory calls = new bytes[](2); calls[0] = abi.encodeCall(IB20Asset.cancelUIMultiplierUpdate, ()); calls[1] = abi.encodeCall(IB20Asset.updateUIMultiplier, (secondMultiplier, secondEffectiveAt)); -asset.announce(calls, "reorder-2026-Q3", "reorder split", "https://disclosures.example/"); +asset.announce(calls, "2026-split-02", "Rescheduled 2-for-1 stock split", "https://example.com/corporate-actions/2026-split-02"); ``` ## Emergency Override -Use `updateMultiplier(newMultiplier)` only when a pending update is wrong and you cannot wait for `effectiveAt`. It applies the value immediately and clears any pending update. +Use `updateMultiplier(newMultiplier)` only when a pending update is wrong and you cannot wait for `effectiveAt`. It applies the value immediately and clears any pending update. The interface guarantees that it emits both `MultiplierUpdated` and `UIMultiplierUpdated`. The base-std stock-split guide documents the sequence as: | Situation | Events (in order) | | --- | --- | -| Live pending (`effectiveAt > block.timestamp`) | `UIMultiplierUpdateCancelled`, then `MultiplierUpdated(new)`, then `UIMultiplierUpdated(old, new, block.timestamp)` | -| Matured or no pending | `MultiplierUpdated(new)`, then `UIMultiplierUpdated(old, new, block.timestamp)` | +| Live pending (`effectiveAt > block.timestamp`) | `UIMultiplierUpdateCancelled`, then `MultiplierUpdated(new)`, then `UIMultiplierUpdated(old, new, effectiveAtTimestamp)` | +| Matured or no pending | `MultiplierUpdated(new)`, then `UIMultiplierUpdated(old, new, effectiveAtTimestamp)` | `MultiplierUpdated` is deprecated. Process only `UIMultiplierUpdated` to avoid handling the same update twice. @@ -146,7 +134,7 @@ Use `updateMultiplier(newMultiplier)` only when a pending update is wrong and yo - If `effectiveAtTimestamp > block.timestamp`, treat the update as pending until that time. - Maturation emits nothing. Do not wait for a second event at the flip. - On `UIMultiplierUpdateCancelled`, discard the pending update. -- Detect a live pending with `effectiveAt() > block.timestamp`. Do **not** check `effectiveAt() == 0`. +- Detect a live pending update with `effectiveAt() > block.timestamp`. Do not check `effectiveAt() == 0`. ## Common Errors @@ -156,8 +144,8 @@ Use `updateMultiplier(newMultiplier)` only when a pending update is wrong and yo | `InvalidMultiplier()` | `newMultiplier` is zero or above `MAX_UI_MULTIPLIER`. | Pass a value in `(0, MAX_UI_MULTIPLIER]`. | | `EffectiveAtInPast(effectiveAt)` | `effectiveAt <= block.timestamp`. | Pass a strictly future timestamp. | | `EffectiveAtTooFar(effectiveAt)` | `effectiveAt > type(uint64).max`. | Pass a timestamp that fits in `uint64`. | -| `UIMultiplierUpdateExists(effectiveAt)` | A live pending update already exists. | Cancel first, or cancel-then-reschedule in one `announce`. | -| `UIMultiplierUpdateDoesNotExist()` | `cancelUIMultiplierUpdate` with no live pending (including after maturity). | Call only while `effectiveAt() > block.timestamp`. | +| `UIMultiplierUpdateExists(effectiveAt)` | A live pending update already exists. | Cancel first, or cancel and reschedule in one `announce`. | +| `UIMultiplierUpdateDoesNotExist()` | `cancelUIMultiplierUpdate` with no live pending update, including after maturity. | Call only while `effectiveAt() > block.timestamp`. | | `AnnouncementIdAlreadyUsed(id)` | `announce` reused an `id`. | Choose a new single-use `id`. | | `InternalCallFailed(call)` | An inner call in `announce` reverted. | Fix the encoded calldata and retry. | diff --git a/docs/build-on-base/issue-rwa/cancel-blocked-units.mdx b/docs/build-on-base/issue-rwa/cancel-blocked-units.mdx index 59a66880b..e83bcbfeb 100644 --- a/docs/build-on-base/issue-rwa/cancel-blocked-units.mdx +++ b/docs/build-on-base/issue-rwa/cancel-blocked-units.mdx @@ -60,7 +60,7 @@ The holder balance and total supply each fall by 100 EXM. -`burnBlocked` destroys shares and reduces `totalSupply`; it does not move them to another account. If you need to move a holder's balance to a safekeeping account without changing `totalSupply`, use `seizeWithMemo` instead. Block the holder under `TRANSFER_SENDER_POLICY` before calling `burnBlocked`. +`burnBlocked` is deprecated. It destroys shares and reduces `totalSupply`; it does not move them to another account. To move a holder's balance to a safekeeping account without changing `totalSupply`, use `seizeWithMemo`, then `burn` if you still need to destroy supply. `burnBlocked` needs `BURN_BLOCKED_ROLE`, is blocked while `BURN` is paused, and reverts `AccountNotBlocked` unless the holder is denied under `TRANSFER_SENDER_POLICY`. ## See Also diff --git a/docs/build-on-base/issue-rwa/create-an-asset-token.mdx b/docs/build-on-base/issue-rwa/create-an-asset-token.mdx index 7f6e07c86..3c1701511 100644 --- a/docs/build-on-base/issue-rwa/create-an-asset-token.mdx +++ b/docs/build-on-base/issue-rwa/create-an-asset-token.mdx @@ -73,7 +73,7 @@ export async function createStockToken() { ``` -The factory assigns a deterministic address from `(variant, sender, salt)`. Address byte `[10]` is `0x00` for Asset tokens. Reusing the same `(variant, sender, salt)` triple reverts `TokenAlreadyExists`. Optional `initCalls` — such as the role grants above — run on the new token in the same transaction; the factory drops access after `createB20` returns. +The factory assigns a deterministic address from `(variant, sender, salt)`. Address byte `[10]` is `0x00` for Asset tokens. Reusing the same `(variant, sender, salt)` triple reverts `TokenAlreadyExists`. Optional `initCalls`, such as the role grants above, run on the new token in the same transaction. The factory drops access after `createB20` returns. See the [B20 token standard](/build-on-base/issue-rwa/create-an-asset-token) for the complete interface, roles, and policies. diff --git a/docs/build-on-base/issue-rwa/pause-transfers.mdx b/docs/build-on-base/issue-rwa/pause-transfers.mdx index b71372102..f9c522d44 100644 --- a/docs/build-on-base/issue-rwa/pause-transfers.mdx +++ b/docs/build-on-base/issue-rwa/pause-transfers.mdx @@ -49,7 +49,7 @@ base-cast send "$TOKEN_ADDRESS" "unpause(uint8[])" "[0]" \ ``` -`pause` and `unpause` each accept a `PausableFeature[]` array. Pass `[0]` to target `TRANSFER` only. The four pausable features are `TRANSFER` (0), `MINT` (1), `BURN` (2), and `SEIZE` (3). Pausing one feature does not affect the others — a holder can still transfer while `MINT` is paused, and a minter can still mint while `TRANSFER` is paused. +`pause` and `unpause` each accept a `PausableFeature[]` array. Pass `[0]` to target `TRANSFER` only. The four pausable features are `TRANSFER` (0), `MINT` (1), `BURN` (2), and `SEIZE` (3). Pausing one feature does not affect the others. A holder can still transfer while `MINT` is paused, and a minter can still mint while `TRANSFER` is paused. Passing an empty array reverts `EmptyFeatureSet`. A feature already in the requested state is a no-op; the call does not revert. After the call, read `isPaused(feature)` to confirm the current state. The event emitted (`Paused` or `Unpaused`) carries the exact array you passed, not the resulting paused set. @@ -60,7 +60,7 @@ Transfer pause state changes without pausing mint or burn. -`pause` requires `PAUSE_ROLE`. `unpause` requires `UNPAUSE_ROLE`. These are separate roles — grant recovery authority more narrowly than emergency pause authority. +`pause` requires `PAUSE_ROLE`. `unpause` requires `UNPAUSE_ROLE`. These are separate roles, so grant recovery authority more narrowly than emergency pause authority. ## See Also diff --git a/docs/build-on-base/issue-stablecoins/issue-your-stablecoin.mdx b/docs/build-on-base/issue-stablecoins/issue-your-stablecoin.mdx index b775ee12c..f5c52842d 100644 --- a/docs/build-on-base/issue-stablecoins/issue-your-stablecoin.mdx +++ b/docs/build-on-base/issue-stablecoins/issue-your-stablecoin.mdx @@ -6,7 +6,7 @@ description: "Create a fiat-backed stablecoin on Base with one B20 factory call. import { StablecoinDemo } from "/snippets/StablecoinDemo.jsx" -Create a fiat-backed token with one call to the [B20 Factory](/specifications/b20/specification-overview#factory), using the `STABLECOIN` variant. Decimals are fixed at `6`, and the token carries an immutable ISO currency code. The type is sealed into the token address at creation and cannot change. +Create a fiat-backed token with one call to the [B20 Factory](/specifications/b20/specification-overview#factory), using the `STABLECOIN` variant. Decimals are fixed at `6`, and the token carries an immutable, self-declared currency code such as `USD`. The type is sealed into the token address at creation and cannot change. ## Demo @@ -25,11 +25,11 @@ New to B20? See the [B20 Token Standard](/specifications/b20/specification-overv Passing `STABLECOIN` to `createB20` writes discriminant byte `0x01` into position `[10]` of the token address. After the call returns: - `decimals()` returns `6` (hardcoded; you do not pass a decimals value). -- `currency()` returns the immutable ISO code you provided (for example `"USD"`). +- `currency()` returns the immutable code you provided (for example `"USD"`). B20 checks the format only, not that the code is a real currency. - The `IB20Stablecoin` surface is live at that address. - Asset-only selectors (`announce`, `updateUIMultiplier`, `batchMint`) do not execute on this address. -The `currency` field is required and must be uppercase ASCII `A`–`Z` only. An empty code reverts `MissingRequiredField`; any other byte reverts `InvalidCurrency`. +The `currency` field is required and must be uppercase ASCII `A` to `Z` only. An empty code reverts `MissingRequiredField`; any other byte reverts `InvalidCurrency`. ## Create and Verify the Stablecoin diff --git a/docs/build-on-base/issue-stablecoins/pause-activity.mdx b/docs/build-on-base/issue-stablecoins/pause-activity.mdx index c6dc25790..f2a7c58d9 100644 --- a/docs/build-on-base/issue-stablecoins/pause-activity.mdx +++ b/docs/build-on-base/issue-stablecoins/pause-activity.mdx @@ -8,7 +8,7 @@ import { StablecoinDemo } from "/snippets/StablecoinDemo.jsx" If something goes wrong, halt activity fast. B20 pausing is **granular**: stop transfers, mints, or burns independently. `PAUSE_ROLE` and `UNPAUSE_ROLE` are separate, so the ability to stop the token can be held more widely than the ability to resume it. -The four pausable features are `TRANSFER`, `MINT`, `BURN`, and `SEIZE` (enum values `0`–`3`). Each is independent — pausing `MINT` and `BURN` leaves `TRANSFER` active. A caller who holds `MINT_ROLE` still cannot mint while `MINT` is paused. +The four pausable features are `TRANSFER`, `MINT`, `BURN`, and `SEIZE` (enum values `0` to `3`). Each is independent: pausing `MINT` and `BURN` leaves `TRANSFER` active. A caller who holds `MINT_ROLE` still cannot mint while `MINT` is paused. ## Demo @@ -60,7 +60,7 @@ See the [B20 token standard](/specifications/b20/specification-overview) for the -`pause` and `unpause` accept a `PausableFeature[]` array. Passing an empty array reverts `EmptyFeatureSet`. Transfer is enum value `0`; never pass a hash such as `keccak256("TRANSFER")`. A feature already in the requested state is a no-op — the call does not revert. +`pause` and `unpause` accept a `PausableFeature[]` array. Passing an empty array reverts `EmptyFeatureSet`. Transfer is enum value `0`; never pass a hash such as `keccak256("TRANSFER")`. A feature already in the requested state is a no-op. The call does not revert. ## Pausable Features @@ -68,7 +68,7 @@ See the [B20 token standard](/specifications/b20/specification-overview) for the | Feature | Enum value | Gates | | --- | --- | --- | | `TRANSFER` | `0` | `transfer`, `transferFrom`, and memo variants | -| `MINT` | `1` | `mint`, `mintWithMemo`, `batchMint` | +| `MINT` | `1` | `mint` and `mintWithMemo` | | `BURN` | `2` | `burn`, `burnWithMemo`, `burnBlocked` | | `SEIZE` | `3` | `seizeWithMemo` | diff --git a/docs/build-on-base/issue-stablecoins/restrict-who-can-hold.mdx b/docs/build-on-base/issue-stablecoins/restrict-who-can-hold.mdx index 73fec1db0..2ddef2679 100644 --- a/docs/build-on-base/issue-stablecoins/restrict-who-can-hold.mdx +++ b/docs/build-on-base/issue-stablecoins/restrict-who-can-hold.mdx @@ -26,7 +26,7 @@ The Policy Registry is a singleton precompile that stores each member list once. Scopes gate specific functions. `TRANSFER_SENDER_POLICY` and `TRANSFER_RECEIVER_POLICY` check the sender and recipient on every `transfer` and `transferFrom`. `MINT_RECEIVER_POLICY` checks the recipient on every `mint`. All three default to `ALWAYS_ALLOW` (`0`) until you bind a policy. -An **allowlist** authorizes only accounts in the set. An empty allowlist authorizes nobody — seed your intended holders before binding the policy. +An **allowlist** authorizes only accounts in the set. An empty allowlist authorizes nobody, so seed your intended holders before binding the policy. ## Create and Bind a Holder Allowlist @@ -91,7 +91,7 @@ An allowlist denies every account not in the policy. Seed intended holders befor ## Update Membership -After creation, only the policy admin can add or remove accounts. Call `updateAllowlist(policyId, true, accounts)` to add and `updateAllowlist(policyId, false, accounts)` to remove. The change is visible to every token that references the policy on the next call — no second `updatePolicy` is needed on the token. +After creation, only the policy admin can add or remove accounts. Call `updateAllowlist(policyId, true, accounts)` to add and `updateAllowlist(policyId, false, accounts)` to remove. The change is visible to every token that references the policy on the next call. No second `updatePolicy` is needed on the token. To combine a KYC allowlist with a sanctions blocklist, create an `INTERSECT` composite policy referencing both simple policies, then bind the composite ID to the token's scopes. See the [B20 token standard](/specifications/b20/specification-overview) for the full policy type reference. diff --git a/docs/docs.json b/docs/docs.json index 52dd7c64a..3f7578903 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -4327,7 +4327,11 @@ }, { "source": "/base-chain/specs/reference/b20/interfaces/IB20/SEIZE_HOLDER_POLICY", - "destination": "/specifications/b20/reference/interfaces/ib20/seize-holder-policy" + "destination": "/specifications/b20/reference/interfaces/ib20/seize-exempt-policy" + }, + { + "source": "/specifications/b20/reference/interfaces/ib20/seize-holder-policy", + "destination": "/specifications/b20/reference/interfaces/ib20/seize-exempt-policy" }, { "source": "/base-chain/specs/reference/b20/interfaces/IB20/SEIZE_RECEIVER_POLICY", diff --git a/docs/specifications/b20/reference/constants-addresses.mdx b/docs/specifications/b20/reference/constants-addresses.mdx index 11c3181a7..d3dc5c3c3 100644 --- a/docs/specifications/b20/reference/constants-addresses.mdx +++ b/docs/specifications/b20/reference/constants-addresses.mdx @@ -37,7 +37,7 @@ These addresses are identical on every network where B20 is active. | Transfer receiver | `keccak256("TRANSFER_RECEIVER_POLICY")` | | Transfer executor | `keccak256("TRANSFER_EXECUTOR_POLICY")` | | Mint receiver | `keccak256("MINT_RECEIVER_POLICY")` | -| Seize holder | `keccak256("SEIZE_HOLDER_POLICY")` | +| Seize exempt | `keccak256("SEIZE_EXEMPT_POLICY")` | | Seize receiver | `keccak256("SEIZE_RECEIVER_POLICY")` | ## Policy IDs diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/create-composite-policy.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/create-composite-policy.mdx index 4496642fc..4c530bbc1 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/create-composite-policy.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/create-composite-policy.mdx @@ -14,20 +14,6 @@ function createCompositePolicy(address admin, PolicyType policyType, uint64[] ca | Selector | `0x6fdd1491` | | Canonical signature | `createCompositePolicy(address,uint8,uint64[])` | -## Parameters - -| Name | Type | Description | -|---|---|---| -| `admin` | `address` | Initial admin authorized to update child policies and transfer or renounce administration. Cannot be `address(0)`. | -| `policyType` | `PolicyType` (`uint8`) | Must be `UNION` or `INTERSECT`. | -| `childPolicyIds` | `uint64[]` | IDs of existing simple policies to combine. Count must be in `[MIN_COMPOSITE_CHILD_POLICIES, MAX_COMPOSITE_CHILD_POLICIES]` (2–4). | - -## Returns - -| Name | Type | Description | -|---|---|---| -| `newPolicyId` | `uint64` | The newly assigned composite policy ID. | - ## Description Creates a composite policy that combines two to four existing simple policies (`ALLOWLIST` or `BLOCKLIST`) under a single logic gate: @@ -41,9 +27,23 @@ The registry stores references to the children, not a snapshot of their members. Creation is permissionless. The `admin` you supply is the only address that can later call `updateComposite`, `stageUpdateAdmin`, or `renounceAdmin` on this policy. -On success, emits `PolicyCreated` and `PolicyAdminUpdated(newPolicyId, address(0), admin)`. +On success, emits `PolicyCreated(newPolicyId, creator, policyType)`, `PolicyAdminUpdated(newPolicyId, address(0), admin)`, and `CompositePolicyUpdated(newPolicyId, creator, childPolicyIds)`. + +## Parameters + +| Name | Type | Description | +|---|---|---| +| `admin` | `address` | Initial admin authorized to update child policies and transfer or renounce administration. Cannot be `address(0)`. | +| `policyType` | `PolicyType` (`uint8`) | Must be `UNION` or `INTERSECT`. | +| `childPolicyIds` | `uint64[]` | IDs of existing simple policies to combine. Count must be in `[MIN_COMPOSITE_CHILD_POLICIES, MAX_COMPOSITE_CHILD_POLICIES]` (2–4). | + +## Returns + +| Name | Type | Description | +|---|---|---| +| `newPolicyId` | `uint64` | The newly assigned composite policy ID. | -## Revert Conditions +## Reverts | Error | Condition | |---|---| @@ -56,11 +56,11 @@ On success, emits `PolicyCreated` and `PolicyAdminUpdated(newPolicyId, address(0 ## Access Control -Permissionless — any caller may create a composite policy. +Permissionless, any caller may create a composite policy. ## Example -```solidity Title Create a KYC-and-sanctions composite +```solidity Title Create a KYC-and-sanctions composite lines wrap expandable highlight={10} // 1. Create a KYC allowlist and a sanctions blocklist first. uint64 kycId = registry.createPolicy(admin, PolicyType.ALLOWLIST); uint64 sanctionsId = registry.createPolicy(admin, PolicyType.BLOCKLIST); @@ -73,9 +73,9 @@ children[1] = sanctionsId; uint64 gateId = registry.createCompositePolicy(admin, PolicyType.INTERSECT, children); // 3. Bind the composite to the token's transfer and mint scopes. -token.updatePolicy(PolicyScope.TRANSFER_SENDER_POLICY, gateId); -token.updatePolicy(PolicyScope.TRANSFER_RECEIVER_POLICY, gateId); -token.updatePolicy(PolicyScope.MINT_RECEIVER_POLICY, gateId); +token.updatePolicy(B20Constants.TRANSFER_SENDER_POLICY, gateId); +token.updatePolicy(B20Constants.TRANSFER_RECEIVER_POLICY, gateId); +token.updatePolicy(B20Constants.MINT_RECEIVER_POLICY, gateId); ``` diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/finalize-update-admin.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/finalize-update-admin.mdx index 3038dcce9..2e54a94dc 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/finalize-update-admin.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/finalize-update-admin.mdx @@ -18,13 +18,22 @@ function finalizeUpdateAdmin(uint64 policyId) external; Completes a two-step admin transfer initiated by `stageUpdateAdmin`. The caller must be the address that was staged as the pending admin. On success, the caller becomes the active admin, the pending slot clears, and the previous admin loses the ability to update the policy. +This function is the second step of the two-step admin transfer pattern: + +1. The current admin calls `stageUpdateAdmin(policyId, newAdmin)`, which sets the pending admin without transferring control. +2. The pending admin calls `finalizeUpdateAdmin(policyId)` to accept and complete the transfer. + +Until `finalizeUpdateAdmin` is called, `policyAdmin(policyId)` still returns the current admin. After a successful call, `policyAdmin(policyId)` returns the new admin and `pendingPolicyAdmin(policyId)` is cleared. + +Emits `PolicyAdminUpdated(policyId, previousAdmin, newAdmin)`. + ## Parameters | Name | Type | Description | |---|---|---| | `policyId` | `uint64` | The policy whose pending admin transfer is being finalized. | -## Revert Conditions +## Reverts | Error | Condition | |---|---| @@ -36,17 +45,6 @@ Completes a two-step admin transfer initiated by `stageUpdateAdmin`. The caller Callable only by the staged pending admin for the target policy. Finalization atomically promotes the caller to active admin and clears the pending slot. The previous admin cannot update the policy after this call succeeds. -## Behavior - -This function is the second step of the two-step admin transfer pattern: - -1. The current admin calls `stageUpdateAdmin(policyId, newAdmin)`, which sets the pending admin without transferring control. -2. The pending admin calls `finalizeUpdateAdmin(policyId)` to accept and complete the transfer. - -Until `finalizeUpdateAdmin` is called, `policyAdmin(policyId)` still returns the current admin. After a successful call, `policyAdmin(policyId)` returns the new admin and `pendingPolicyAdmin(policyId)` is cleared. - -Emits `PolicyAdminUpdated(policyId, previousAdmin, newAdmin)`. - ## Example ```solidity Usage Example diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/pending-policy-admin.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/pending-policy-admin.mdx index 1508dbd15..0439e33f9 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/pending-policy-admin.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/pending-policy-admin.mdx @@ -18,7 +18,7 @@ function pendingPolicyAdmin(uint64 policyId) external view returns (address); Returns the currently-staged pending admin for `policyId`, or `address(0)` when no transfer is in flight or for built-in sentinels, unknown IDs, and malformed IDs. Never reverts. -The pending admin is set by `stageUpdateAdmin(policyId, newAdmin)` and cleared when `finalizeUpdateAdmin(policyId)` succeeds or when the current admin calls `stageUpdateAdmin` with `address(0)`. Until `finalizeUpdateAdmin` is called, `policyAdmin` still returns the current admin — the pending admin has no privileges yet. +The pending admin is set by `stageUpdateAdmin(policyId, newAdmin)` and cleared when `finalizeUpdateAdmin(policyId)` succeeds or when the current admin calls `stageUpdateAdmin` with `address(0)`. Until `finalizeUpdateAdmin` is called, `policyAdmin` still returns the current admin, the pending admin has no privileges yet. ## Parameters @@ -32,16 +32,10 @@ The pending admin is set by `stageUpdateAdmin(policyId, newAdmin)` and cleared w |---|---| | `address` | Staged pending admin, or `address(0)` if none is set. | -## Access Control - -Read-only. No role required. - -## Revert Conditions +## Reverts Never reverts. -## Related +## Access Control -- `stageUpdateAdmin` — sets the pending admin. -- `finalizeUpdateAdmin` — promotes the pending admin to current admin; reverts `NoPendingAdmin` if nothing is staged, or `Unauthorized` if the caller is not the staged address. -- `policyAdmin` — returns the current (active) admin. +Read-only. No role required. diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/policy-admin.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/policy-admin.mdx index 89c73ed95..28ef9b180 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/policy-admin.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/policy-admin.mdx @@ -14,6 +14,14 @@ function policyAdmin(uint64 policyId) external view returns (address); | Selector | `0x09dd0a47` | | Canonical signature | `policyAdmin(uint64)` | +## Description + +Never reverts. Returns `address(0)` for any ID that has no admin, including sentinels and IDs that have never been created. + +After `renounceAdmin`, no address can be assigned as admin again. `policyAdmin` returns `address(0)` permanently for that policy. + +During a pending admin transfer, `policyAdmin` still returns the **current** admin. The staged nominee is readable via `pendingPolicyAdmin`. Administration transfers only when the nominee calls `finalizeUpdateAdmin`. + ## Parameters | Name | Type | Description | @@ -24,17 +32,9 @@ function policyAdmin(uint64 policyId) external view returns (address); The current admin address, or `address(0)` for built-in sentinels (`ALWAYS_ALLOW`, `ALWAYS_BLOCK`), renounced policies, unknown IDs, and malformed IDs. -## Behavior - -Never reverts. Returns `address(0)` for any ID that has no admin — including sentinels and IDs that have never been created. - -After `renounceAdmin`, no address can be assigned as admin again. `policyAdmin` returns `address(0)` permanently for that policy. - -During a pending admin transfer, `policyAdmin` still returns the **current** admin. The staged nominee is readable via `pendingPolicyAdmin`. Administration transfers only when the nominee calls `finalizeUpdateAdmin`. - ## Access Control -View — no role required. +View, no role required. ## Example diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/renounce-admin.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/renounce-admin.mdx index f61f42e12..f568e6128 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/renounce-admin.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/renounce-admin.mdx @@ -18,7 +18,10 @@ function renounceAdmin(uint64 policyId) external; Permanently relinquishes administration of `policyId`. After this call, the member set is frozen: no one can add or remove members, replace composite children, or assign a new admin. `isAuthorized` queries continue to work normally. -Use this when you want to guarantee that a policy's membership can never change — for example, to make a compliance list immutable. +Use this when you want to guarantee that a policy's membership can never change, for example, to make a compliance list immutable. + +- Administration is gone permanently. There is no call that assigns a new admin after renounce. +- Emits `PolicyAdminUpdated(policyId, previousAdmin, address(0))`. ## Parameters @@ -26,7 +29,7 @@ Use this when you want to guarantee that a policy's membership can never change |---|---|---| | `policyId` | `uint64` | The policy whose administration is being renounced. | -## Revert Conditions +## Reverts | Error | Condition | |---|---| @@ -37,12 +40,6 @@ Use this when you want to guarantee that a policy's membership can never change Callable only by the current policy admin for `policyId`. -## Behavior Notes - -- Administration is gone permanently. There is no call that assigns a new admin after renounce. -- A staged pending admin (set via `stageUpdateAdmin`) is also cleared — `finalizeUpdateAdmin` cannot be used to recover the policy. -- Emits `PolicyAdminUpdated(policyId, previousAdmin, address(0))`. - ## Example ```solidity Usage Example diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/stage-update-admin.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/stage-update-admin.mdx index d749edd6c..991f0ee2f 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/stage-update-admin.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/stage-update-admin.mdx @@ -14,14 +14,7 @@ function stageUpdateAdmin(uint64 policyId, address newAdmin) external; | Selector | `0x1d7ae695` | | Canonical signature | `stageUpdateAdmin(uint64,address)` | -## Parameters - -| Name | Type | Description | -|---|---|---| -| `policyId` | `uint64` | Policy whose admin is being staged. | -| `newAdmin` | `address` | Proposed new admin. Pass `address(0)` to clear a nomination that has not been finalized. | - -## Behavior +## Description Nominates `newAdmin` as the pending admin for `policyId`. The current admin does not change. `policyAdmin(policyId)` still returns the current admin. `pendingPolicyAdmin(policyId)` returns `newAdmin`. @@ -31,23 +24,30 @@ Passing `address(0)` as `newAdmin` clears any previously staged nomination witho Emits `PolicyAdminStaged(policyId, currentAdmin, pendingAdmin)`. -## Access Control +## Parameters -Callable only by the current policy admin for `policyId`. Any other caller reverts `Unauthorized`. +| Name | Type | Description | +|---|---|---| +| `policyId` | `uint64` | Policy whose admin is being staged. | +| `newAdmin` | `address` | Proposed new admin. Pass `address(0)` to clear a nomination that has not been finalized. | -## Revert Conditions +## Reverts | Error | Condition | |---|---| | `PolicyNotFound()` | `policyId` does not exist in the registry. | | `Unauthorized()` | Caller is not the current admin of `policyId`. | +## Access Control + +Callable only by the current policy admin for `policyId`. Any other caller reverts `Unauthorized`. + ## Example ```solidity Title Admin handoff -// Step 1 — current admin nominates a successor +// Step 1: current admin nominates a successor IPolicyRegistry(registry).stageUpdateAdmin(policyId, nextAdmin); -// Step 2 — nextAdmin finalizes (separate transaction, called by nextAdmin) +// Step 2: nextAdmin finalizes (separate transaction, called by nextAdmin) IPolicyRegistry(registry).finalizeUpdateAdmin(policyId); ``` diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/update-allowlist.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/update-allowlist.mdx index 6681e0b95..b288c69ea 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/update-allowlist.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/update-allowlist.mdx @@ -16,9 +16,9 @@ function updateAllowlist(uint64 policyId, bool allowed, address[] calldata accou ## Description -Sets each address in `accounts` to `allowed` (add or remove) in an `ALLOWLIST` policy. The change takes effect on the next `isAuthorized` call against this policy. Every token and composite that references `policyId` sees the updated membership immediately — no second write on the token is needed. +Sets each address in `accounts` to `allowed` (add or remove) in an `ALLOWLIST` policy. The change takes effect on the next `isAuthorized` call against this policy. Every token and composite that references `policyId` sees the updated membership immediately, no second write on the token is needed. -Membership batches are capped at 64 accounts (`MAX_BATCH_SIZE`). +Membership batches are capped by the registry, currently at 64 accounts. A larger batch reverts `BatchSizeTooLarge(maxBatchSize)`, which carries the limit. ## Parameters @@ -28,11 +28,7 @@ Membership batches are capped at 64 accounts (`MAX_BATCH_SIZE`). | `allowed` | `bool` | `true` to add accounts to the set; `false` to remove them. | | `accounts` | `address[]` | Accounts to update. Maximum 64 per call. | -## Access Control - -Callable only by the current admin of `policyId`. Any other caller reverts `Unauthorized`. - -## Revert Conditions +## Reverts | Error | Condition | |---|---| @@ -45,10 +41,17 @@ Callable only by the current admin of `policyId`. Any other caller reverts `Unau Emits `AllowlistUpdated(policyId, updater, allowed, accounts)` on success. +## Access Control + +Callable only by the current admin of `policyId`. Any other caller reverts `Unauthorized`. + ## Example ```solidity Title Add accounts to an allowlist -IPolicyRegistry(registry).updateAllowlist(kycId, true, [alice, bob]); +address[] memory accounts = new address[](2); +accounts[0] = alice; +accounts[1] = bob; +IPolicyRegistry(registry).updateAllowlist(kycId, true, accounts); ``` After this call, `isAuthorized(kycId, alice)` and `isAuthorized(kycId, bob)` return `true`. Any token or composite policy already pointing at `kycId` reflects this without a separate `updatePolicy` call on the token. @@ -56,7 +59,9 @@ After this call, `isAuthorized(kycId, alice)` and `isAuthorized(kycId, bob)` ret To remove an account, pass `false`: ```solidity Title Remove an account from an allowlist -IPolicyRegistry(registry).updateAllowlist(kycId, false, [alice]); +address[] memory removed = new address[](1); +removed[0] = alice; +IPolicyRegistry(registry).updateAllowlist(kycId, false, removed); ``` diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/update-blocklist.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/update-blocklist.mdx index 32070d6cd..852f7877e 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/update-blocklist.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/update-blocklist.mdx @@ -14,29 +14,25 @@ function updateBlocklist(uint64 policyId, bool blocked, address[] calldata accou | Selector | `0x5c4e51b8` | | Canonical signature | `updateBlocklist(uint64,bool,address[])` | -## Parameters - -| Name | Type | Description | -|---|---|---| -| `policyId` | `uint64` | The BLOCKLIST policy to update. | -| `blocked` | `bool` | Membership state to apply to every account in the batch. `true` adds accounts to the blocklist; `false` removes them. | -| `accounts` | `address[]` | Accounts to update. Capped at 64 per call. | - -## Behavior +## Description Sets each address in `accounts` to the membership state `blocked` within the identified BLOCKLIST policy. A BLOCKLIST authorizes any account that is **not** in the set, so adding an account (`blocked = true`) causes `isAuthorized` to return `false` for that account, and removing it (`blocked = false`) restores authorization. -The change takes effect on the next `isAuthorized` query. Every token and composite policy that references `policyId` sees the updated result immediately — no second `updatePolicy` call is needed on any token. +The change takes effect on the next `isAuthorized` query. Every token and composite policy that references `policyId` sees the updated result immediately, no second `updatePolicy` call is needed on any token. -Batches are capped at `64` accounts (`MAX_BATCH_SIZE`). Exceeding this limit reverts with `BatchSizeTooLarge`. +Batches are capped by the registry, currently at 64 accounts. A larger batch reverts `BatchSizeTooLarge(maxBatchSize)`, which carries the limit. Emits `BlocklistUpdated(policyId, updater, blocked, accounts)` on success. -## Access Control +## Parameters -Callable only by the current admin of `policyId`. Any other caller reverts `Unauthorized`. +| Name | Type | Description | +|---|---|---| +| `policyId` | `uint64` | The BLOCKLIST policy to update. | +| `blocked` | `bool` | Membership state to apply to every account in the batch. `true` adds accounts to the blocklist; `false` removes them. | +| `accounts` | `address[]` | Accounts to update. Capped at 64 per call. | -## Revert Conditions +## Reverts | Error | Condition | |---|---| @@ -45,12 +41,21 @@ Callable only by the current admin of `policyId`. Any other caller reverts `Unau | `IncompatiblePolicyType()` | The policy is not a `BLOCKLIST`. | | `BatchSizeTooLarge(maxBatchSize)` | `accounts.length` exceeds 64. | +## Access Control + +Callable only by the current admin of `policyId`. Any other caller reverts `Unauthorized`. + ## Example -```solidity Title Add accounts to a blocklist +```solidity Title Add accounts to a blocklist lines wrap expandable highlight={5,10} // Add two addresses to an existing BLOCKLIST policy. -IPolicyRegistry(registry).updateBlocklist(sanctionsId, true, [Carol, Dave]); - -// Remove Carol from the blocklist. -IPolicyRegistry(registry).updateBlocklist(sanctionsId, false, [Carol]); +address[] memory blocked = new address[](2); +blocked[0] = carol; +blocked[1] = dave; +IPolicyRegistry(registry).updateBlocklist(sanctionsId, true, blocked); + +// Remove carol from the blocklist. +address[] memory unblocked = new address[](1); +unblocked[0] = carol; +IPolicyRegistry(registry).updateBlocklist(sanctionsId, false, unblocked); ``` diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/update-composite.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/update-composite.mdx index 2a7ff229d..398976834 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/update-composite.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/update-composite.mdx @@ -16,7 +16,7 @@ function updateComposite(uint64 policyId, uint64[] calldata childPolicyIds) exte ## Description -Replaces a composite policy's child-policy set in full with `childPolicyIds`. There is no partial edit — the entire child set is replaced in one call. +Replaces a composite policy's child-policy set in full with `childPolicyIds`. There is no partial edit, the entire child set is replaced in one call. The write takes effect on the next `isAuthorized` call that references `policyId`. Every token that stores this policy ID sees the new result without a second `updatePolicy` call on the token. @@ -27,11 +27,7 @@ The write takes effect on the next `isAuthorized` call that references `policyId | `policyId` | `uint64` | Composite policy to update. Must exist and be a `UNION` or `INTERSECT` policy. | | `childPolicyIds` | `uint64[]` | Complete new set of existing simple policy IDs. Count must be in `[MIN_COMPOSITE_CHILD_POLICIES, MAX_COMPOSITE_CHILD_POLICIES]` (2–4). | -## Access Control - -Callable only by the current admin of `policyId`. A composite whose admin has been renounced (via `renounceAdmin`) can never be updated. - -## Revert Conditions +## Reverts | Error | Condition | |---|---| @@ -39,12 +35,16 @@ Callable only by the current admin of `policyId`. A composite whose admin has be | `PolicyNotFound()` | `policyId` does not exist, or any entry in `childPolicyIds` does not exist. | | `IncompatiblePolicyType()` | `policyId` is not a composite (`UNION` or `INTERSECT`). | | `ChildPoliciesOutsideOfRange()` | `childPolicyIds.length` is outside `[MIN_COMPOSITE_CHILD_POLICIES, MAX_COMPOSITE_CHILD_POLICIES]` (2–4). | -| `InvalidChildPolicy(childPolicyId)` | A child policy is itself a composite rather than a simple (`ALLOWLIST` or `BLOCKLIST`) policy. | +| `InvalidChildPolicy(childPolicyId)` | A child is a composite or a built-in sentinel (`ALWAYS_ALLOW`, `ALWAYS_BLOCK`) rather than a simple `ALLOWLIST` or `BLOCKLIST` policy. | ## Events Emits `CompositePolicyUpdated(policyId, updater, childPolicyIds)` on success. +## Access Control + +Callable only by the current admin of `policyId`. A composite whose admin has been renounced (via `renounceAdmin`) can never be updated. + ## Example ```solidity Usage Example diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/announce.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/announce.mdx index 11054d8de..81c379d24 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/announce.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/announce.mdx @@ -25,6 +25,11 @@ Emits `Announcement(caller, id, description, uri)`, executes every entry in `int Indexers pair open and close brackets by `id`. Every event emitted between the two logs belongs to the announced action. A state-changing call that appears outside this bracket was invoked directly, not through `announce`. +- Execution order: emit `Announcement` → run inner calls → emit `EndAnnouncement`. If any inner call fails, the whole transaction reverts and `id` is not consumed. +- After a successful call, `isAnnouncementIdUsed(id)` returns `true`. +- `announce` is defined on **B20 Asset** only. Stablecoin has no announcements. +- Typical inner calls: `updateUIMultiplier`, `cancelUIMultiplierUpdate`, `batchMint`, `mintWithMemo`, `burnWithMemo`. + ## Parameters | Name | Type | Description | @@ -34,11 +39,7 @@ Indexers pair open and close brackets by `id`. Every event emitted between the t | `description` | `string` | Human-readable summary shown to holders. The asset does not verify this string. | | `uri` | `string` | Off-chain URI for the full announcement record. The asset does not verify this string. | -## Access Control - -`OPERATOR_ROLE` gates this call. Any other caller reverts `AccessControlUnauthorizedAccount`. Inner calls keep their own role gates; the operator must hold every role the inner calls require (for example `MINT_ROLE` for `batchMint`, `BURN_ROLE` for `burnWithMemo`). - -## Revert Conditions +## Reverts | Error | Condition | |---|---| @@ -50,12 +51,9 @@ Indexers pair open and close brackets by `id`. Every event emitted between the t A Solidity `Panic` inside an inner call (for example arithmetic overflow) propagates raw and is not wrapped as `InternalCallFailed`. -## Behavior Notes +## Access Control -- Execution order: emit `Announcement` → run inner calls → emit `EndAnnouncement`. If any inner call fails, the whole transaction reverts and `id` is not consumed. -- After a successful call, `isAnnouncementIdUsed(id)` returns `true`. -- `announce` is defined on **B20 Asset** only. Stablecoin has no announcements. -- Typical inner calls: `updateUIMultiplier`, `cancelUIMultiplierUpdate`, `batchMint`, `mintWithMemo`, `burnWithMemo`. +`OPERATOR_ROLE` gates this call. Any other caller reverts `AccessControlUnauthorizedAccount`. Inner calls keep their own role gates; the operator must hold every role the inner calls require (for example `MINT_ROLE` for `batchMint`, `BURN_ROLE` for `burnWithMemo`). ## Example @@ -71,9 +69,3 @@ asset.announce(calls, id, description, uri); asset.announce(new bytes[](0), id, description, uri); // Emits: Announcement → EndAnnouncement ``` - -## Related - -- `isAnnouncementIdUsed(string id)` — selector `0xc0da474e` -- `Announcement(address indexed caller, string id, string description, string uri)` — topic0 `0xccebf8218a62875909564adef86a6f4df81503cb617221e793357d62f8e813f7` -- `EndAnnouncement(string id)` — topic0 `0x96d64dafe2c790596430196b982ad1da3221cb3b0f4e6e2df77f2e4f71a90037` diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/batch-mint.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/batch-mint.mdx index 1e1a31c9c..db49e3008 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/batch-mint.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/batch-mint.mdx @@ -25,15 +25,7 @@ Mints `amounts[i]` to `recipients[i]` in one call. All-or-nothing: any element r | `recipients` | `address[]` | Accounts receiving the minted tokens. | | `amounts` | `uint256[]` | Per-recipient amounts, parallel to `recipients`. | -## Access Control - -Caller must hold `MINT_ROLE`. Reverts `AccessControlUnauthorizedAccount` otherwise. - -## Policy Interaction - -Each recipient must pass `MINT_RECEIVER_POLICY`. Reverts `PolicyForbids(MINT_RECEIVER_POLICY, ...)` for any recipient that does not. - -## Revert Conditions +## Reverts | Error | Condition | |---|---| @@ -45,9 +37,17 @@ Each recipient must pass `MINT_RECEIVER_POLICY`. Reverts `PolicyForbids(MINT_REC | `PolicyForbids(MINT_RECEIVER_POLICY, ...)` | Any recipient is not authorized by the mint receiver policy. | | `SupplyCapExceeded` | Cumulative mint would exceed the supply cap. | +## Access Control + +Caller must hold `MINT_ROLE`. Reverts `AccessControlUnauthorizedAccount` otherwise. + +## Policy Interaction + +Each recipient must pass `MINT_RECEIVER_POLICY`. Reverts `PolicyForbids(MINT_RECEIVER_POLICY, ...)` for any recipient that does not. + ## Example -```solidity Title Usage Example +```solidity Title Usage Example lines wrap expandable highlight={9} address[] memory recipients = new address[](2); recipients[0] = holderA; recipients[1] = holderB; diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/effective-at.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/effective-at.mdx index 76a5efdd6..61d2af444 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/effective-at.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/effective-at.mdx @@ -18,25 +18,16 @@ function effectiveAt() external view returns (uint256); Returns the `block.timestamp` at which the pending UI multiplier becomes active. -A live pending update exists while `effectiveAt() > block.timestamp`. Once `block.timestamp >= effectiveAt()`, `uiMultiplier()` returns the new multiplier automatically — no transaction is required and no event is emitted at maturation. +A live pending update exists while `effectiveAt() > block.timestamp`. Once `block.timestamp >= effectiveAt()`, `uiMultiplier()` returns the new multiplier automatically, no transaction is required and no event is emitted at maturation. After the flip, check for a new live pending update with `effectiveAt() > block.timestamp`. Do not check `effectiveAt() == 0`. -## Behavior - | State | Return value | |---|---| | No pending update | `0` | | Pending update scheduled | Future timestamp set by `updateUIMultiplier` | -| Pending update matured | `0` (cleared after flip) | +| Pending update matured | The scheduled timestamp, now in the past. Only `cancelUIMultiplierUpdate` resets the value to `0`. | -Maturation is time-based and storage-free. Indexers must not wait for an on-chain event at `effectiveAt`. The `UIMultiplierUpdated` event fires when the update is **recorded**, not when it matures. +Maturation is time-based. Indexers must not wait for an onchain event at `effectiveAt`. The `UIMultiplierUpdated` event fires when the update is **recorded**, not when it matures. - -## Related - -- `uiMultiplier()` — returns the current effective multiplier. -- `newUIMultiplier()` — returns the scheduled multiplier value. -- `updateUIMultiplier(uint256, uint256)` — schedules a multiplier change and emits `UIMultiplierUpdated`. -- `cancelUIMultiplierUpdate()` — clears a live pending update before `effectiveAt`. diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/multiplier.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/multiplier.mdx index 1b5b561fe..4ae33e2f7 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/multiplier.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/multiplier.mdx @@ -18,18 +18,14 @@ function multiplier() external view returns (uint256); Returns the effective multiplier at `block.timestamp`, scaled to `WAD_PRECISION` (`1e18`). Holder balances are stored as raw ERC-20 units; the multiplier scales them into a derived UI view without rewriting those balances. -This function is a deprecated alias of the ERC-8056 `uiMultiplier()`. Prefer `uiMultiplier()`. +This function is an alias of the ERC-8056 `uiMultiplier()`. Both return the same value; integrators should prefer the ERC-8056 name. -A default multiplier of `WAD_PRECISION` (`1e18`) means UI equals raw. A 2-for-1 split is encoded as `2e18`; a 1-for-2 reverse split is `5e17`. +A multiplier of `WAD_PRECISION` (`1e18`) means UI equals raw. A 2-for-1 split is encoded as `2e18`; a 1-for-2 reverse split is `5e17`. The UI balance for a holder is `balanceOf(account) * multiplier() / WAD_PRECISION`. Use `balanceOfUI(account)` to read that derived value directly. If a pending update has been scheduled via `updateUIMultiplier`, this function still returns the current multiplier until `block.timestamp >= effectiveAt()`. The pending value is readable from `newUIMultiplier()`. - -This is a deprecated alias. Use `uiMultiplier()` (ERC-8056) instead. Both return the same value. - - ## Access Control View only. No role required. @@ -41,6 +37,6 @@ No direct policy interaction. ## Example ```solidity Usage Example -IB20Asset(target).multiplier(); // deprecated alias -IB20Asset(target).uiMultiplier(); // preferred ERC-8056 name +IB20Asset(target).multiplier(); // B20 name +IB20Asset(target).uiMultiplier(); // ERC-8056 name, same value ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/operator-role.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/operator-role.mdx index 5379b9fd5..309069f2f 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/operator-role.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/operator-role.mdx @@ -18,10 +18,10 @@ function OPERATOR_ROLE() external view returns (bytes32); Returns the `bytes32` role constant required to call: -- `announce` — disclose a corporate action -- `updateUIMultiplier` — schedule a future multiplier change -- `cancelUIMultiplierUpdate` — cancel a live pending multiplier update -- `updateMultiplier` — immediate override (deprecated; emergency use only) +- `announce`: disclose a corporate action +- `updateUIMultiplier`: schedule a future multiplier change +- `cancelUIMultiplierUpdate`: cancel a live pending multiplier update +- `updateMultiplier`: immediate override (deprecated; emergency use only) The metadata setters (`updateName`, `updateSymbol`, `updateExtraMetadata`) are gated by `METADATA_ROLE`, not `OPERATOR_ROLE`. diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/scaled-balance-of.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/scaled-balance-of.mdx index 152d0ad33..fd82b35bc 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/scaled-balance-of.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/scaled-balance-of.mdx @@ -3,10 +3,6 @@ title: "IB20Asset.scaledBalanceOf" description: "Returns the UI (display) balance of an account by applying the current multiplier to its raw ERC-20 balance." --- - -`scaledBalanceOf` is a deprecated alias. Prefer `balanceOfUI(account)` for new integrations. - - ## Signature ```solidity IB20Asset.sol @@ -24,7 +20,12 @@ Equivalent to `toUIAmount(balanceOf(account))`. Returns the caller-visible share The raw ERC-20 balance is unchanged. Only the displayed value scales with the multiplier. -This function is a deprecated alias of `balanceOfUI(account)`. Prefer `balanceOfUI` for new code. +`balanceOfUI(account)` from the ERC-8056 Balances extension returns the same value; integrators should prefer the ERC-8056 name. + +- While a pending multiplier update exists (`effectiveAt() > block.timestamp`), this function still uses the **current** multiplier, not the scheduled one. +- After `block.timestamp >= effectiveAt()`, the new multiplier takes effect automatically on read: no transaction or event fires at maturation. +- Integer division rounds down. Rounding loss is confined to the scaled view and is at most one unit of the scaled amount when `uiMultiplier() != WAD_PRECISION`. +- This function applies only to B20 Asset. B20 Stablecoin has no multiplier. ## Parameters @@ -38,13 +39,6 @@ This function is a deprecated alias of `balanceOfUI(account)`. Prefer `balanceOf |---|---| | `uint256` | UI balance: `balanceOf(account) * uiMultiplier() / WAD_PRECISION`. | -## Multiplier Behavior - -- While a pending multiplier update exists (`effectiveAt() > block.timestamp`), this function still uses the **current** multiplier, not the scheduled one. -- After `block.timestamp >= effectiveAt()`, the new multiplier takes effect automatically on read — no transaction or event fires at maturation. -- Integer division rounds down. A round trip through `toUIAmount` then `fromUIAmount` can lose one unit in the last place when `uiMultiplier() != WAD_PRECISION`. -- This function applies only to B20 Asset. B20 Stablecoin has no multiplier. - ## Access Control Read-only. No role required. @@ -56,9 +50,7 @@ No direct policy interaction. ## Example ```solidity Usage Example -// Deprecated alias — prefer balanceOfUI uint256 uiBalance = IB20Asset(target).scaledBalanceOf(account); - -// Preferred -uint256 uiBalance = IB20Asset(target).balanceOfUI(account); +// Same value through the ERC-8056 name: +uint256 uiBalanceErc8056 = IB20Asset(target).balanceOfUI(account); ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/to-scaled-balance.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/to-scaled-balance.mdx index 8eb8dedb5..d5cbaab08 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/to-scaled-balance.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/to-scaled-balance.mdx @@ -26,7 +26,7 @@ This is a deprecated alias of `toUIAmount`. The two functions are equivalent. Us The result is the amount that wallets and indexers display as a share count after a stock split. Raw ERC-20 balances, `transfer` amounts, `totalSupply`, and allowances are unaffected by the multiplier and remain raw. -Integer division rounds down. A round trip through `toUIAmount` then `fromUIAmount` can lose one unit in the last place (ULP) when `uiMultiplier() != WAD_PRECISION`. +Integer division rounds down. Rounding loss is confined to the scaled view and is at most one unit of the scaled amount; a round trip back through `fromUIAmount` may return slightly less than the original raw amount when `uiMultiplier() != WAD_PRECISION`. Multipliers are Asset-only. Stablecoin has no multiplier. @@ -53,16 +53,9 @@ No direct policy interaction. ## Example ```solidity Usage Example -// Deprecated alias — prefer toUIAmount +// Deprecated alias; prefer toUIAmount uint256 ui = IB20Asset(target).toScaledBalance(rawBalance); // Preferred equivalent uint256 ui = IB20Asset(target).toUIAmount(rawBalance); ``` - -## Related - -- `toUIAmount` — preferred replacement. -- `fromUIAmount` (`toRawBalance`) — inverse conversion. -- `balanceOfUI` (`scaledBalanceOf`) — applies the same formula to a holder's stored balance. -- `uiMultiplier` — returns the effective multiplier at `block.timestamp`. diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/to-ui-amount.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/to-ui-amount.mdx index e20d3e611..21800a575 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/to-ui-amount.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/to-ui-amount.mdx @@ -16,12 +16,17 @@ function toUIAmount(uint256 rawAmount) external view returns (uint256); ## Description -Converts `rawAmount` to its UI representation at the effective multiplier. The formula is `rawAmount * uiMultiplier() / WAD_PRECISION`, where `WAD_PRECISION` is `1e18`. Integer division rounds down, so a round trip through `toUIAmount` then `fromUIAmount` can lose one unit in the last place (ULP) when the multiplier differs from `WAD_PRECISION`. +Converts `rawAmount` to its UI representation at the effective multiplier. The formula is `rawAmount * uiMultiplier() / WAD_PRECISION`, where `WAD_PRECISION` is `1e18`. Integer division rounds down. Rounding loss is confined to the scaled view and is at most one unit of the scaled amount; a round trip back through `fromUIAmount` may return slightly less than the original raw amount when the multiplier differs from `WAD_PRECISION`. -The effective multiplier is time-dependent. If a pending update has been scheduled via `updateUIMultiplier` and `block.timestamp >= effectiveAt`, the new multiplier is used even though no storage write or event fires at that moment. Call `uiMultiplier()` to inspect the value this function applies. +The effective multiplier is time-dependent. If a pending update has been scheduled via `updateUIMultiplier` and `block.timestamp >= effectiveAt`, the new multiplier is used even though no event fires at that moment. Call `uiMultiplier()` to inspect the value this function applies. `toScaledBalance` is a deprecated alias of this function. Prefer `toUIAmount`. +- This function is Asset-only. Stablecoin has no multiplier. +- A multiplier of `1e18` means UI equals raw. +- A 2-for-1 split sets the multiplier to `2e18`; a 1-for-2 reverse split sets it to `5e17`. +- The multiplier is an absolute value, not a ratio applied to the current one. + ## Parameters | Name | Type | Description | @@ -33,10 +38,3 @@ The effective multiplier is time-dependent. If a pending update has been schedul | Type | Description | |---|---| | `uint256` | UI amount: `rawAmount * uiMultiplier() / 1e18`, rounded down. | - -## Notes - -- This function is Asset-only. Stablecoin has no multiplier. -- A default multiplier of `1e18` means UI equals raw. -- A 2-for-1 split sets the multiplier to `2e18`; a 1-for-2 reverse split sets it to `5e17`. -- The multiplier is an absolute value, not a ratio applied to the current one. diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/ui-multiplier.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/ui-multiplier.mdx index 769ce150d..ac3117131 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/ui-multiplier.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/ui-multiplier.mdx @@ -20,24 +20,12 @@ Returns the effective UI multiplier at `block.timestamp`, represented with 18 de Raw ERC-20 balances are never rewritten during a split. The multiplier changes the displayed scale only. A wallet or indexer derives the UI share count as: -``` +```text Title UI balance formula ui = balanceOf(account) * uiMultiplier() / 1e18 ``` A default (no-split) state returns `1e18`. A 2-for-1 split returns `2e18`. A 1-for-2 reverse split returns `5e17`. -## Timing - -`uiMultiplier()` reflects a scheduled change **at or after** `effectiveAt`, not when `updateUIMultiplier` is called. Before `effectiveAt`, it returns the current multiplier. There is no event emitted at the flip — indexers must not wait for a log at `effectiveAt`. +`uiMultiplier()` reflects a scheduled change **at or after** `effectiveAt`, not when `updateUIMultiplier` is called. Before `effectiveAt`, it returns the current multiplier. There is no event emitted at the flip, indexers must not wait for a log at `effectiveAt`. To inspect a live pending update, read `newUIMultiplier()` and `effectiveAt()`. A pending update is active while `effectiveAt() > block.timestamp`. - -## Related - -- `newUIMultiplier()` — the scheduled multiplier value, if any. -- `effectiveAt()` — the timestamp at which the pending multiplier takes effect. -- `balanceOfUI(account)` — convenience view: `balanceOf(account) * uiMultiplier() / 1e18`. -- `totalSupplyUI()` — same formula applied to `totalSupply()`. -- `toUIAmount(raw)` / `fromUIAmount(ui)` — convert single amounts at the current multiplier. -- `updateUIMultiplier(newMultiplier, effectiveAt)` — schedules a future multiplier change. -- `multiplier()` — deprecated alias of `uiMultiplier()`. diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/update-multiplier.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/update-multiplier.mdx index 2e4bfa6e8..60a528cb2 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/update-multiplier.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/update-multiplier.mdx @@ -24,10 +24,6 @@ Applies `newMultiplier` immediately at `block.timestamp` and clears any live pen The multiplier is an 18-decimal WAD: `1e18` is `1.0` (`WAD_PRECISION`). A 2-for-1 split is `2e18`. A reverse split is a value below `1e18`. -## Access Control - -Caller must hold `OPERATOR_ROLE`. Any other caller reverts `AccessControlUnauthorizedAccount(caller, OPERATOR_ROLE)`. - ## Parameters | Name | Type | Description | @@ -43,15 +39,19 @@ Caller must hold `OPERATOR_ROLE`. Any other caller reverts `AccessControlUnautho ## Events -Event order depends on whether a pending update exists at call time: +The interface guarantees that the call emits both `MultiplierUpdated` and `UIMultiplierUpdated`. The base-std stock-split guide documents the sequence as: | Situation | Events (in order) | |---|---| -| Live pending (`effectiveAt > block.timestamp`) | `UIMultiplierUpdateCancelled`, then `MultiplierUpdated(new)`, then `UIMultiplierUpdated(old, new, block.timestamp)` | -| Matured or no pending | `MultiplierUpdated(new)`, then `UIMultiplierUpdated(old, new, block.timestamp)` | +| Live pending (`effectiveAt > block.timestamp`) | `UIMultiplierUpdateCancelled`, then `MultiplierUpdated(new)`, then `UIMultiplierUpdated(old, new, effectiveAtTimestamp)` | +| Matured or no pending | `MultiplierUpdated(new)`, then `UIMultiplierUpdated(old, new, effectiveAtTimestamp)` | `MultiplierUpdated` is itself deprecated. Integrators should process only `UIMultiplierUpdated` to avoid handling the same update twice. +## Access Control + +Caller must hold `OPERATOR_ROLE`. Any other caller reverts `AccessControlUnauthorizedAccount(caller, OPERATOR_ROLE)`. + ## Policy Interaction No direct policy interaction. diff --git a/docs/specifications/b20/reference/interfaces/ib20-factory/create-b20.mdx b/docs/specifications/b20/reference/interfaces/ib20-factory/create-b20.mdx index a25563d27..1795be2cf 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-factory/create-b20.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-factory/create-b20.mdx @@ -16,10 +16,18 @@ function createB20(B20Variant variant, bytes32 salt, bytes calldata params, byte ## Description -Creates a B20 token of the given `variant` at the deterministic address derived from `(variant, msg.sender, salt)`. The Factory encodes the variant choice into the token address: byte `[0]` is `0xB2`, byte `[10]` is the variant discriminant (`0x00` for Asset, `0x01` for Stablecoin). The Factory then plants a `0xef` bytecode stub at that address — the stub the node reads to recognize a live B20 — seals the token's identity (name, symbol, decimals, and variant-specific fields), emits `B20Created`, grants the initial admin role (skipped when `address(0)` is passed), and dispatches each entry in `initCalls` on the new token. Returns the token address. +Creates a B20 token of the given `variant` at the deterministic address derived from `(variant, msg.sender, salt)`. The Factory encodes the variant choice into the token address: byte `[0]` is `0xB2`, byte `[10]` is the variant discriminant (`0x00` for Asset, `0x01` for Stablecoin). The Factory then plants a `0xef` bytecode stub at that address, the stub the node reads to recognize a live B20, seals the token's identity (name, symbol, decimals, and variant-specific fields), emits `B20Created`, grants the initial admin role (skipped when `address(0)` is passed), and dispatches each entry in `initCalls` on the new token. Returns the token address. The type is chosen once. After `createB20` returns, the variant encoded in the address cannot change. The Factory retains no persisted access after the call returns. +During the `createB20` call, factory-originated calls bypass the token's role gates and its transfer-side policy gates (`TRANSFER_SENDER_POLICY`, `TRANSFER_RECEIVER_POLICY`, `TRANSFER_EXECUTOR_POLICY`). This lets `initCalls` perform admin setup, `grantRole`, `updatePolicy`, `updateSupplyCap`, bootstrap transfers, without the Factory holding any role. The bypass is not total: + +- **`MINT_RECEIVER_POLICY` is always enforced**, including for factory-originated mints. An `initCalls` bundle that sets a restrictive `MINT_RECEIVER_POLICY` and then mints to a non-authorized account reverts `PolicyForbids(MINT_RECEIVER_POLICY, ...)`, bubbled out of `createB20`. +- **Pause is never bypassed.** Pause defaults to nothing-paused at creation. To start paused, sequence the `pause(...)` call last among `initCalls`. +- **Token invariants are never bypassed.** Supply-cap math and balance accounting always apply. + +The window closes when `createB20` returns. + ## Parameters | Name | Type | Description | @@ -35,7 +43,7 @@ The type is chosen once. After `createB20` returns, the variant encoded in the a |---|---|---| | `token` | `address` | Address of the newly created token. | -## Revert Conditions +## Reverts | Error | Condition | |---|---| @@ -43,22 +51,12 @@ The type is chosen once. After `createB20` returns, the variant encoded in the a | `FeatureNotActivated` | The variant feature (`B20Asset` or `B20Stablecoin`) is not activated in the Activation Registry. | | `InvalidVariant` | `variant` is outside the `B20Variant` range. | | `UnsupportedVersion` | The leading `version` byte in `params` is unrecognized for the given `variant`. | -| `MissingRequiredField` | A required string field is empty — for example, Stablecoin `currency`. | +| `MissingRequiredField` | A required string field is empty: for example, Stablecoin `currency`. | | `InvalidCurrency` | Stablecoin `currency` is non-empty but contains a byte outside `A`–`Z`. | | `InvalidDecimals` | Asset `decimals` is outside `[B20Constants.MIN_ASSET_DECIMALS, B20Constants.MAX_ASSET_DECIMALS]` (i.e., outside `[6, 18]`). | | `TokenAlreadyExists` | A token already exists at the derived address. Reusing `(variant, sender, salt)` always hits this. | | `InitCallFailed` | An entry in `initCalls` reverted. The inner reason is bubbled when available. | -## Bootstrap Window - -During the `createB20` call, factory-originated calls bypass the token's role gates and its transfer-side policy gates (`TRANSFER_SENDER_POLICY`, `TRANSFER_RECEIVER_POLICY`, `TRANSFER_EXECUTOR_POLICY`). This lets `initCalls` perform admin setup — `grantRole`, `updatePolicy`, `updateSupplyCap`, bootstrap transfers — without the Factory holding any role. The bypass is not total: - -- **`MINT_RECEIVER_POLICY` is always enforced**, including for factory-originated mints. An `initCalls` bundle that sets a restrictive `MINT_RECEIVER_POLICY` and then mints to a non-authorized account reverts `PolicyForbids(MINT_RECEIVER_POLICY, ...)`, bubbled out of `createB20`. -- **Pause is never bypassed.** Pause defaults to nothing-paused at creation. To start paused, sequence the `pause(...)` call last among `initCalls`. -- **Token invariants are never bypassed.** Supply-cap math and balance accounting always apply. - -The window closes when `createB20` returns. - ## Access Control No role required. Any caller may invoke `createB20`. The caller's address is part of the address derivation `(variant, msg.sender, salt)`. diff --git a/docs/specifications/b20/reference/interfaces/ib20-factory/is-b20-initialized.mdx b/docs/specifications/b20/reference/interfaces/ib20-factory/is-b20-initialized.mdx index 677e4df7e..d678dd48c 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-factory/is-b20-initialized.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-factory/is-b20-initialized.mdx @@ -18,7 +18,7 @@ function isB20Initialized(address token) external view returns (bool); Returns `true` if and only if `token` bears the `0xB2` address prefix **and** the `0xef` bytecode stub that `createB20` plants when it creates a token. This is the stronger existence check: `isB20(address)` tests the prefix alone and can return `true` for an address the Factory has never touched, whereas `isB20Initialized` confirms the Factory has run to completion at that address. -The flag flips exactly once — the moment the creating `createB20` call returns. It never reverts. +The flag flips exactly once, the moment the creating `createB20` call returns. It never reverts. Before `createB20` is called, the predicted token address matches the `0xB2` prefix but carries no stub. Calls to that address are no-ops. Only after `createB20` returns does the `0xef` stub exist, enabling dynamic routing to the token's native logic. @@ -36,13 +36,13 @@ Before `createB20` is called, the predicted token address matches the `0xB2` pre |---|---| | `bool` | `true` if `token` is a fully initialized B20 (prefix + stub present); `false` otherwise. | -## Access Control +## Reverts -View function. No role required. +Never reverts. -## Revert Conditions +## Access Control -Never reverts. +View function. No role required. ## Example diff --git a/docs/specifications/b20/reference/interfaces/ib20-factory/is-b20.mdx b/docs/specifications/b20/reference/interfaces/ib20-factory/is-b20.mdx index 4e1b72d45..e1d089323 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-factory/is-b20.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-factory/is-b20.mdx @@ -19,7 +19,7 @@ function isB20(address token) external view returns (bool); Returns `true` when `token` bears the `0xB2` address prefix (byte `[0]` is `0xB2`, bytes `[1:9]` are zero). The check reads the address directly; it does not consult a registry or any on-chain state. Never reverts. -`isB20` is a prefix-only check. It can return `true` for an address the Factory has not yet created. Use `isB20Initialized` when you need to confirm the token is live — that stronger check also requires the `0xef` bytecode stub the Factory plants during `createB20`. +`isB20` is a prefix-only check. It can return `true` for an address the Factory has not yet created. Use `isB20Initialized` when you need to confirm the token is live, that stronger check also requires the `0xef` bytecode stub the Factory plants during `createB20`. ## Parameters @@ -36,7 +36,7 @@ Returns `true` when `token` bears the `0xB2` address prefix (byte `[0]` is `0xB2 ## Access Control -View — no access restriction. +View, no access restriction. ## Policy Interaction diff --git a/docs/specifications/b20/reference/interfaces/ib20/burn-blocked.mdx b/docs/specifications/b20/reference/interfaces/ib20/burn-blocked.mdx index ef20ae476..d272ce00e 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/burn-blocked.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/burn-blocked.mdx @@ -20,7 +20,7 @@ function burnBlocked(address from, uint256 amount) external; DEPRECATED. Burns `amount` from a `from` account that is blocked under `TRANSFER_SENDER_POLICY`, without spending an allowance. Retained for back-compat; prefer `seizeWithMemo` (to a treasury/self address) followed by `burn`. Emits `Transfer(from, address(0), amount)` and `BurnedBlocked(caller, from, amount)`. -Gated by `BURN_BLOCKED_ROLE` and the `BURN` pause vector (not `SEIZE`). Its blocked check reads `TRANSFER_SENDER_POLICY`, distinct from `seizeWithMemo`'s `SEIZE_HOLDER_POLICY`. +Gated by `BURN_BLOCKED_ROLE` and the `BURN` pause vector (not `SEIZE`). Its blocked check reads `TRANSFER_SENDER_POLICY`, distinct from `seizeWithMemo`'s `SEIZE_EXEMPT_POLICY`. ## Parameters diff --git a/docs/specifications/b20/reference/interfaces/ib20/grant-role.mdx b/docs/specifications/b20/reference/interfaces/ib20/grant-role.mdx index e6b3bb6fc..21f7ea8f2 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/grant-role.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/grant-role.mdx @@ -16,7 +16,7 @@ function grantRole(bytes32 role, address account) external; ## Description -Grants `role` to `account`. If `account` does not already hold `role`, emits `RoleGranted(role, account, sender)`. If `account` already holds `role`, the call is a no-op and emits nothing. +Grants `role` to `account` and emits `RoleGranted(role, account, sender)`. B20 follows OpenZeppelin AccessControl semantics, under which granting a role the account already holds changes nothing and emits no event. The caller must hold the admin role of `role`. On a fresh token every role's admin is `DEFAULT_ADMIN_ROLE`. That mapping can be changed with `setRoleAdmin`. After `renounceLastAdmin` is called, no address holds `DEFAULT_ADMIN_ROLE` and `grantRole` becomes permanently unreachable for any role whose admin chain includes `DEFAULT_ADMIN_ROLE`. diff --git a/docs/specifications/b20/reference/interfaces/ib20/index.mdx b/docs/specifications/b20/reference/interfaces/ib20/index.mdx index 65a60ca09..f970307c2 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/index.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/index.mdx @@ -20,7 +20,7 @@ description: "Generated B20 reference for IB20 functions, events, and errors." | [`TRANSFER_RECEIVER_POLICY`](/specifications/b20/reference/interfaces/ib20/transfer-receiver-policy) | `0x210f521b` | Policy slot consulted against `to` on every transfer. | | [`TRANSFER_EXECUTOR_POLICY`](/specifications/b20/reference/interfaces/ib20/transfer-executor-policy) | `0x724e9c53` | Policy slot consulted against `msg.sender` on `transferFrom` when distinct from `from`. | | [`MINT_RECEIVER_POLICY`](/specifications/b20/reference/interfaces/ib20/mint-receiver-policy) | `0x6e5b013d` | Policy slot consulted against `to` on every mint. | -| [`SEIZE_HOLDER_POLICY`](/specifications/b20/reference/interfaces/ib20/seize-holder-policy) | `0xb279d311` | Policy slot consulted against `from` by `seizeWithMemo`. | +| [`SEIZE_EXEMPT_POLICY`](/specifications/b20/reference/interfaces/ib20/seize-exempt-policy) | `0xfeb346ec` | Policy slot consulted against `from` by `seizeWithMemo`. | | [`SEIZE_RECEIVER_POLICY`](/specifications/b20/reference/interfaces/ib20/seize-receiver-policy) | `0xb31da27f` | Policy slot consulted against `to` by `seizeWithMemo`. | | [`name`](/specifications/b20/reference/interfaces/ib20/name) | `0x06fdde03` | Token name. Set at creation, mutable via `updateName`. | | [`symbol`](/specifications/b20/reference/interfaces/ib20/symbol) | `0x95d89b41` | Token symbol. Set at creation, mutable via `updateSymbol`. | @@ -49,9 +49,9 @@ description: "Generated B20 reference for IB20 functions, events, and errors." | [`renounceLastAdmin`](/specifications/b20/reference/interfaces/ib20/renounce-last-admin) | `0x6f79e3d7` | Permanently transitions the token to a zero-admin state. Revokes `DEFAULT_ADMIN_ROLE` | | [`setRoleAdmin`](/specifications/b20/reference/interfaces/ib20/set-role-admin) | `0x1e4e0091` | Sets the admin role for `role`. Emits `RoleAdminChanged`. | | [`pausedFeatures`](/specifications/b20/reference/interfaces/ib20/paused-features) | `0xde9997e3` | The `PausableFeature`s currently paused on this token. Order is implementation-defined; | -| [`isPaused`](/specifications/b20/reference/interfaces/ib20/is-paused) | `0x165c44bc` | Whether `feature` is currently paused. O(1). | -| [`pause`](/specifications/b20/reference/interfaces/ib20/pause) | `0x9f98bd2d` | Pauses each of `features`. Additive: features already paused remain paused; duplicates | -| [`unpause`](/specifications/b20/reference/interfaces/ib20/unpause) | `0xb61b2ebc` | Unpauses each of `features`. Features not listed are unaffected; duplicates are idempotent. | +| [`isPaused`](/specifications/b20/reference/interfaces/ib20/is-paused) | `0xbc61e733` | Whether `feature` is currently paused. O(1). | +| [`pause`](/specifications/b20/reference/interfaces/ib20/pause) | `0xa290249c` | Pauses each of `features`. Additive: features already paused remain paused; duplicates | +| [`unpause`](/specifications/b20/reference/interfaces/ib20/unpause) | `0x8b93dd63` | Unpauses each of `features`. Features not listed are unaffected; duplicates are idempotent. | | [`policyId`](/specifications/b20/reference/interfaces/ib20/policy-id) | `0xdb3de624` | The current policy ID configured for `policyScope`. Returns `0` (always-allow built-in) | | [`updatePolicy`](/specifications/b20/reference/interfaces/ib20/update-policy) | `0xadf9c4ea` | Updates the policy ID assigned to `policyScope`. Takes effect immediately for the next | | [`supplyCap`](/specifications/b20/reference/interfaces/ib20/supply-cap) | `0x8f770ad0` | The maximum total supply enforced on `mint`. Capped at `type(uint128).max`, which | diff --git a/docs/specifications/b20/reference/interfaces/ib20/is-paused.mdx b/docs/specifications/b20/reference/interfaces/ib20/is-paused.mdx index dc4a69aad..7046b8ad5 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/is-paused.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/is-paused.mdx @@ -11,8 +11,8 @@ function isPaused(PausableFeature feature) external view returns (bool); | Field | Value | |---|---| -| Selector | `0x165c44bc` | -| Canonical signature | `isPaused(PausableFeature)` | +| Selector | `0xbc61e733` | +| Canonical signature | `isPaused(uint8)` | ## Description @@ -27,7 +27,11 @@ Returns whether `feature` is currently paused. O(1). | `BURN` | 2 | `burn`, `burnWithMemo`, and the deprecated `burnBlocked` | | `SEIZE` | 3 | `seizeWithMemo` | -The paused state for all features is stored as a bitmask in a single storage word; each feature's bit is its ordinal. `isPaused` reads that bit for the requested feature. +The ABI encodes `PausableFeature` as `uint8`, so callers pass the ordinal. + +- A paused feature blocks the operations listed in the table above regardless of whether the caller holds the relevant role. For example, a `MINT_ROLE` holder cannot mint while `MINT` is paused. +- If an operation is attempted while its feature is paused, the transaction reverts with `ContractPaused(feature)`. +- Use `pausedFeatures()` to read the full current paused set in one call. ## Parameters @@ -41,18 +45,12 @@ The paused state for all features is stored as a bitmask in a single storage wor ## Access Control -View function — no role required. +View function, no role required. ## Policy Interaction No direct policy interaction. -## Behavior Notes - -- A paused feature blocks the operations listed in the table above regardless of whether the caller holds the relevant role. For example, a `MINT_ROLE` holder cannot mint while `MINT` is paused. -- If an operation is attempted while its feature is paused, the transaction reverts with `ContractPaused(feature)`. -- Use `pausedFeatures()` to read the full current paused set in one call. - ## Example ```solidity Usage Example diff --git a/docs/specifications/b20/reference/interfaces/ib20/pause.mdx b/docs/specifications/b20/reference/interfaces/ib20/pause.mdx index a3d246397..c38d3d47d 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/pause.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/pause.mdx @@ -11,12 +11,12 @@ function pause(PausableFeature[] calldata features) external; | Field | Value | |---|---| -| Selector | `0x9f98bd2d` | -| Canonical signature | `pause(PausableFeature[])` | +| Selector | `0xa290249c` | +| Canonical signature | `pause(uint8[])` | ## Description -Pauses each feature in `features`. The call is additive: features already paused remain paused, and duplicates within the array are silent no-ops. Emits `Paused(updater, features)` with the exact array passed. +Pauses each feature in `features`. The call is additive: features already paused remain paused, and duplicates within the array are silent no-ops. Emits `Paused(updater, features)` with the exact array passed. While a feature is paused, the operations it gates revert `ContractPaused(feature)`. ## Parameters @@ -24,22 +24,10 @@ Pauses each feature in `features`. The call is additive: features already paused |---|---| | `features` | One or more `PausableFeature` values to pause. Must be non-empty. | -## PausableFeature values - -| Value | Gates | -|---|---| -| `TRANSFER` | `transfer`, `transferFrom`, and memo variants | -| `MINT` | `mint`, `mintWithMemo`, and Asset `batchMint` | -| `BURN` | `burn`, `burnWithMemo`, and deprecated `burnBlocked` | -| `SEIZE` | `seizeWithMemo` | - -Each feature corresponds to one bit in a single storage word (`TRANSFER` = bit 0, `MINT` = bit 1, `BURN` = bit 2, `SEIZE` = bit 3). `ALL_FEATURES_PAUSED` (`15`) means all four bits are set. - ## Reverts -- `AccessControlUnauthorizedAccount(account, neededRole)` — caller does not hold `PAUSE_ROLE`. -- `EmptyFeatureSet()` — `features.length == 0`. -- `ContractPaused(feature)` — emitted by downstream calls when a paused feature is exercised; not thrown by `pause` itself. +- `AccessControlUnauthorizedAccount(account, neededRole)`: caller does not hold `PAUSE_ROLE`. +- `EmptyFeatureSet()`: `features.length == 0`. ## Access Control @@ -49,6 +37,17 @@ Each feature corresponds to one bit in a single storage word (`TRANSFER` = bit 0 No direct policy interaction. +## Pausable Features + +| Value | Gates | +|---|---| +| `TRANSFER` | `transfer`, `transferFrom`, and memo variants | +| `MINT` | `mint`, `mintWithMemo`, and Asset `batchMint` | +| `BURN` | `burn`, `burnWithMemo`, and deprecated `burnBlocked` | +| `SEIZE` | `seizeWithMemo` | + +The ABI encodes `PausableFeature` as `uint8`, in enum order (`TRANSFER` = 0, `MINT` = 1, `BURN` = 2, `SEIZE` = 3). `B20Constants.ALL_FEATURES_PAUSED` (`15`) is the bitmask with all four set. + ## Example ```solidity Usage Example diff --git a/docs/specifications/b20/reference/interfaces/ib20/paused-features.mdx b/docs/specifications/b20/reference/interfaces/ib20/paused-features.mdx index f7bfddac1..0be8fd697 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/paused-features.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/paused-features.mdx @@ -16,10 +16,14 @@ function pausedFeatures() external view returns (PausableFeature[] memory); ## Description -Returns every `PausableFeature` that is currently paused on this token. The four possible values are `TRANSFER`, `MINT`, `BURN`, and `SEIZE`. The paused set is stored as one bit per feature in a single storage word; this function decodes that bitmap into an array. Order is implementation-defined — treat the result as a set. +Returns every `PausableFeature` that is currently paused on this token. The four possible values are `TRANSFER`, `MINT`, `BURN`, and `SEIZE`. Order is implementation-defined, so treat the result as a set. To check a single feature, prefer `isPaused(feature)`. +- The returned array reflects the state at the block the call executes in. +- `B20Constants.ALL_FEATURES_PAUSED` (`15`) is the bitmask with every feature set. +- Pausing is controlled by `pause(PausableFeature[])` (requires `PAUSE_ROLE`) and reversed by `unpause(PausableFeature[])` (requires `UNPAUSE_ROLE`). When a feature is paused, operations it gates revert `ContractPaused(feature)`. + ## Returns | Name | Type | Description | @@ -28,18 +32,12 @@ To check a single feature, prefer `isPaused(feature)`. ## Access Control -View — no role required. +View, no role required. ## Policy Interaction No direct policy interaction. -## Behavior Notes - -- The returned array reflects the state at the block the call executes in. -- `ALL_FEATURES_PAUSED` (`15`) means all four bits are set; this function returns all four enum members in that case. -- Pausing is controlled by `pause(PausableFeature[])` (requires `PAUSE_ROLE`) and reversed by `unpause(PausableFeature[])` (requires `UNPAUSE_ROLE`). When a feature is paused, operations it gates revert `ContractPaused(feature)`. - ## Example ```solidity Usage Example diff --git a/docs/specifications/b20/reference/interfaces/ib20/policy-id.mdx b/docs/specifications/b20/reference/interfaces/ib20/policy-id.mdx index fca63e0bd..9849a2036 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/policy-id.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/policy-id.mdx @@ -16,7 +16,7 @@ function policyId(bytes32 policyScope) external view returns (uint64); ## Description -Returns the policy ID stored for `policyScope`. If the scope has never been assigned, this returns `0`, which is the `ALWAYS_ALLOW` built-in sentinel — every address is authorized. +Returns the policy ID stored for `policyScope`. If the scope has never been assigned, this returns `0`, which is the `ALWAYS_ALLOW` built-in sentinel, every address is authorized. ## Parameters @@ -30,7 +30,7 @@ The `uint64` policy ID currently bound to the scope. `0` means `ALWAYS_ALLOW`. ## Reverts -- `UnsupportedPolicyType(policyScope)` — when `policyScope` is not a slot this token supports. +- `UnsupportedPolicyType(policyScope)`: when `policyScope` is not a slot this token supports. ## Access Control @@ -46,7 +46,7 @@ The following scopes are recognized by a B20 token. Pass one of these as `policy | `TRANSFER_RECEIVER_POLICY` | `transfer`, `transferFrom`, and memo variants | `to` | | `TRANSFER_EXECUTOR_POLICY` | `transferFrom` and `transferFromWithMemo` when `msg.sender != from` | `msg.sender` | | `MINT_RECEIVER_POLICY` | `mint`, `mintWithMemo`, `batchMint` | `to` | -| `SEIZE_HOLDER_POLICY` | `seizeWithMemo` | `from` | +| `SEIZE_EXEMPT_POLICY` | `seizeWithMemo` | `from` | | `SEIZE_RECEIVER_POLICY` | `seizeWithMemo` | `to` | An unset scope reads as `0` (`ALWAYS_ALLOW`). To bind a different policy, the token admin calls `updatePolicy(policyScope, newPolicyId)`. diff --git a/docs/specifications/b20/reference/interfaces/ib20/revoke-role.mdx b/docs/specifications/b20/reference/interfaces/ib20/revoke-role.mdx index dc969b12f..2b70e69ed 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/revoke-role.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/revoke-role.mdx @@ -16,7 +16,7 @@ function revokeRole(bytes32 role, address account) external; ## Description -Revokes `role` from `account`. The call is idempotent — if `account` does not hold `role`, the call succeeds and emits nothing. `RoleRevoked` fires only when membership actually changes. +Revokes `role` from `account` and emits `RoleRevoked(role, account, sender)`. B20 follows OpenZeppelin AccessControl semantics, under which revoking a role the account does not hold changes nothing and emits no event. ## Parameters @@ -27,8 +27,8 @@ Revokes `role` from `account`. The call is idempotent — if `account` does not ## Reverts -- `AccessControlUnauthorizedAccount` — caller does not hold the admin role for `role`, or the token has no admins (transitioned via `renounceLastAdmin`). -- `LastAdminCannotRenounce` — `role == DEFAULT_ADMIN_ROLE` and `account` is the last remaining `DEFAULT_ADMIN_ROLE` holder. Use `renounceLastAdmin` to clear the final admin intentionally. +- `AccessControlUnauthorizedAccount`: caller does not hold the admin role for `role`, or the token has no admins (transitioned via `renounceLastAdmin`). +- `LastAdminCannotRenounce`: `role == DEFAULT_ADMIN_ROLE` and `account` is the last remaining `DEFAULT_ADMIN_ROLE` holder. Use `renounceLastAdmin` to clear the final admin intentionally. ## Access Control diff --git a/docs/specifications/b20/reference/interfaces/ib20/seize-exempt-policy.mdx b/docs/specifications/b20/reference/interfaces/ib20/seize-exempt-policy.mdx new file mode 100644 index 000000000..e3d8e7f90 --- /dev/null +++ b/docs/specifications/b20/reference/interfaces/ib20/seize-exempt-policy.mdx @@ -0,0 +1,55 @@ +--- +title: "IB20.SEIZE_EXEMPT_POLICY" +description: "Returns the policy scope identifier consulted against the `from` address in seizeWithMemo. An authorized account is seize-exempt." +--- + +## Signature + +```solidity IB20.sol +function SEIZE_EXEMPT_POLICY() external view returns (bytes32); +``` + +| Field | Value | +|---|---| +| Selector | `0xfeb346ec` | +| Canonical signature | `SEIZE_EXEMPT_POLICY()` | + +## Description + +Policy slot consulted against `from` by `seizeWithMemo`. An authorized account is seize-exempt. + +The check is inverted relative to the transfer and mint scopes: `seizeWithMemo` proceeds only when `isAuthorized(policyId, from)` returns `false`. When the slot is unset (`0`), the always-allow sentinel applies, every account is authorized, and every account is seize-exempt. You must attach a policy before any holder can be seized. + +The value is `keccak256("SEIZE_EXEMPT_POLICY")`, `0xedb5da348cfb67af08746d3afd1be81034b50d5c8576f31aff688f39dfd540ed`. Before Cobalt this scope was named `SEIZE_HOLDER_POLICY`; see the [seize changelog entry](/base-chain/specs/reference/b20/changelog/02-cobalt-b20-seize). + +## Returns + +`bytes32` policy scope constant. Pass this value to `updatePolicy` and `policyId`. + +## Access Control + +View function. No role required. + +## Policy Interaction + +The policy ID stored at this scope is read by `seizeWithMemo` before moving any balance. The recommended policy type is `BLOCKLIST`: + +- Accounts in the blocklist have `isAuthorized = false`, so they are seizable. +- Accounts not in the blocklist have `isAuthorized = true`, so they are seize-exempt. + +An `ALLOWLIST` or the `ALWAYS_BLOCK` sentinel inverts this logic and makes every account seizable. Do not attach either to `SEIZE_EXEMPT_POLICY`. + +This scope is independent of `TRANSFER_SENDER_POLICY`. Blocking a holder from transferring does not make them seizable. + +## Example + +```solidity Title Make one holder seizable lines wrap expandable +// Create a blocklist and add the holder +uint64 seizableId = registry.createPolicy(policyAdmin, IPolicyRegistry.PolicyType.BLOCKLIST); +address[] memory holders = new address[](1); +holders[0] = alice; +registry.updateBlocklist(seizableId, true, holders); + +// Attach it to the seize-exempt scope; alice is now seizable, everyone else is exempt +IB20(token).updatePolicy(IB20(token).SEIZE_EXEMPT_POLICY(), seizableId); +``` diff --git a/docs/specifications/b20/reference/interfaces/ib20/seize-holder-policy.mdx b/docs/specifications/b20/reference/interfaces/ib20/seize-holder-policy.mdx deleted file mode 100644 index b626cebc4..000000000 --- a/docs/specifications/b20/reference/interfaces/ib20/seize-holder-policy.mdx +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: "IB20.SEIZE_HOLDER_POLICY" -description: "Returns the policy scope identifier consulted against the `from` address in seizeWithMemo." ---- - -## Signature - -```solidity IB20.sol -function SEIZE_HOLDER_POLICY() external view returns (bytes32); -``` - -| Field | Value | -|---|---| -| Selector | `0xb279d311` | -| Canonical signature | `SEIZE_HOLDER_POLICY()` | - -## Description - -Policy slot consulted against `from` by `seizeWithMemo`. - -A `from` is seizable only when it is **not** authorized by this policy. The check is inverted relative to transfer and mint scopes: `seizeWithMemo` proceeds only when `isAuthorized(policyId, from)` returns `false`. - -When the slot is unset (`0`), the sentinel resolves to always-allow, meaning every account is authorized and therefore no account is seizable. You must attach a policy before any holder can be seized. - -## Returns - -`bytes32` policy scope constant. Pass this value to `updatePolicy` and `policyId`. - -## Access Control - -View function. No role required. - -## Policy Interaction - -The policy ID stored at this scope is read by `seizeWithMemo` before moving any balance. The recommended policy type is `BLOCKLIST`: - -- Accounts **in** the blocklist have `isAuthorized = false` → seizable. -- Accounts **not in** the blocklist have `isAuthorized = true` → not seizable. - -An `ALLOWLIST` or the `ALWAYS_BLOCK` sentinel inverts this logic and makes every account seizable. Do not attach either to `SEIZE_HOLDER_POLICY`. - -## Example - -```solidity Usage Example -// Read the scope constant -bytes32 scope = IB20(token).SEIZE_HOLDER_POLICY(); - -// Attach a blocklist policy to that scope -IB20(token).updatePolicy(scope, seizableId); - -// Read back the attached policy ID -uint64 id = IB20(token).policyId(scope); -``` diff --git a/docs/specifications/b20/reference/interfaces/ib20/seize-receiver-policy.mdx b/docs/specifications/b20/reference/interfaces/ib20/seize-receiver-policy.mdx index ca44b867c..6512f334c 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/seize-receiver-policy.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/seize-receiver-policy.mdx @@ -22,7 +22,7 @@ The check is a normal allow check: the call proceeds only when `isAuthorized(pol ## Returns -`bytes32` — the policy scope constant for the seize receiver slot. +`bytes32`, the policy scope constant for the seize receiver slot. ## Access Control @@ -38,7 +38,7 @@ Attach a policy ID to this scope with `updatePolicy(token.SEIZE_RECEIVER_POLICY( ## Example -```solidity Title Restrict seize destinations to a treasury +```solidity Title Restrict seize destinations to a treasury lines wrap expandable highlight={11} // Read the scope key bytes32 scope = IB20(token).SEIZE_RECEIVER_POLICY(); diff --git a/docs/specifications/b20/reference/interfaces/ib20/seize-role.mdx b/docs/specifications/b20/reference/interfaces/ib20/seize-role.mdx index 74209ba85..439ed61e1 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/seize-role.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/seize-role.mdx @@ -20,7 +20,7 @@ Returns the `bytes32` role constant that gates `seizeWithMemo`. Any account that ## Returns -`bytes32` — the `SEIZE_ROLE` constant. +`bytes32`, the `SEIZE_ROLE` constant. ## Access Control @@ -28,9 +28,9 @@ Read-only. Anyone may call this function to retrieve the role identifier. ## Policy Interaction -No direct policy interaction. The role returned here is used alongside `SEIZE_HOLDER_POLICY` and `SEIZE_RECEIVER_POLICY` to authorize a seize operation. The caller must hold `SEIZE_ROLE`, the `from` account must not be authorized under `SEIZE_HOLDER_POLICY` (i.e., an inverted blocklist check), and the `to` account must be authorized under `SEIZE_RECEIVER_POLICY`. +No direct policy interaction. The role returned here is used alongside `SEIZE_EXEMPT_POLICY` and `SEIZE_RECEIVER_POLICY` to authorize a seize operation. The caller must hold `SEIZE_ROLE`, the `from` account must not be authorized under `SEIZE_EXEMPT_POLICY` (i.e., an inverted blocklist check), and the `to` account must be authorized under `SEIZE_RECEIVER_POLICY`. -## Usage +## Example Grant this role to the account that will call `seizeWithMemo`: @@ -43,5 +43,5 @@ token.seizeWithMemo(from, to, amount, memo); ``` -`SEIZE_ROLE` is separate from pause control. A holder of `SEIZE_ROLE` cannot seize while `PausableFeature.SEIZE` is paused — the call reverts `ContractPaused(SEIZE)`. Pausing `TRANSFER`, `MINT`, or `BURN` does not affect seize. +`SEIZE_ROLE` is separate from pause control. A holder of `SEIZE_ROLE` cannot seize while `PausableFeature.SEIZE` is paused, the call reverts `ContractPaused(SEIZE)`. Pausing `TRANSFER`, `MINT`, or `BURN` does not affect seize. diff --git a/docs/specifications/b20/reference/interfaces/ib20/seize-with-memo.mdx b/docs/specifications/b20/reference/interfaces/ib20/seize-with-memo.mdx index 159f7d03d..e428febcb 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/seize-with-memo.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/seize-with-memo.mdx @@ -24,10 +24,10 @@ Seize skips allowance and the transfer policies (`TRANSFER_SENDER_POLICY`, `TRAN | Scope | Account | Passes when | |---|---|---| -| `SEIZE_HOLDER_POLICY` | `from` | `isAuthorized` returns `false` (inverted check) | +| `SEIZE_EXEMPT_POLICY` | `from` | `isAuthorized` returns `false` (inverted check) | | `SEIZE_RECEIVER_POLICY` | `to` | `isAuthorized` returns `true` | -`SEIZE_HOLDER_POLICY` is inverted: when the scope is unset (ID `0`, always-allow), every account is authorized and therefore no account is seizable. Attach a blocklist and add the holder to make them seizable. +`SEIZE_EXEMPT_POLICY` is inverted: when the scope is unset (ID `0`, always-allow), every account is authorized and therefore no account is seizable. Attach a blocklist and add the holder to make them seizable. `SEIZE_RECEIVER_POLICY` defaults to always-allow when unset, so an unconfigured token accepts any destination. @@ -48,25 +48,25 @@ Seize skips allowance and the transfer policies (`TRANSFER_SENDER_POLICY`, `TRAN | `AccessControlUnauthorizedAccount(caller, SEIZE_ROLE)` | Caller does not hold `SEIZE_ROLE`. | | `InvalidReceiver(to)` | `to` is `address(0)`, or `from == to`. | | `InvalidSender(from)` | `from` is `address(0)`. | -| `AccountNotSeizable(from)` | `from` is authorized under `SEIZE_HOLDER_POLICY` (scope unset, or holder not on the attached blocklist). | +| `AccountNotSeizable(from)` | `from` is authorized under `SEIZE_EXEMPT_POLICY` (scope unset, or holder not on the attached blocklist). | | `PolicyForbids(SEIZE_RECEIVER_POLICY, policyId)` | `to` is not authorized under `SEIZE_RECEIVER_POLICY`. | | `InsufficientBalance(from, balance, amount)` | `from`'s balance is below `amount`. | -## Access Control - -`SEIZE_ROLE` gates this function. The `SEIZE` pausable feature provides a second switch: a caller holding `SEIZE_ROLE` still cannot seize while `SEIZE` is paused. - ## Events Emitted on success, in this order: 1. `Transfer(from, to, amount)` 2. `Memo(caller, memo)` -3. `Seized(caller, from, to, amount)` — topic0: `0xa9aec5d8b86e2fa2fd6ac3af62f2622e3dfdab1967d4cbbb56a5df7d74cb887c` +3. `Seized(caller, from, to, amount)`: topic0: `0xa9aec5d8b86e2fa2fd6ac3af62f2622e3dfdab1967d4cbbb56a5df7d74cb887c` + +## Access Control + +`SEIZE_ROLE` gates this function. The `SEIZE` pausable feature provides a second switch: a caller holding `SEIZE_ROLE` still cannot seize while `SEIZE` is paused. ## Example -```solidity Usage Example +```solidity Usage Example lines wrap expandable highlight={9,19} // Grant the seizer role token.grantRole(token.SEIZE_ROLE(), seizer); @@ -75,7 +75,7 @@ uint64 seizableId = POLICY_REGISTRY.createPolicy(policyAdmin, IPolicyRegistry.Po address[] memory holders = new address[](1); holders[0] = alice; POLICY_REGISTRY.updateBlocklist(seizableId, true, holders); -token.updatePolicy(token.SEIZE_HOLDER_POLICY(), seizableId); +token.updatePolicy(token.SEIZE_EXEMPT_POLICY(), seizableId); // Optionally restrict destinations uint64 destId = POLICY_REGISTRY.createPolicy(policyAdmin, IPolicyRegistry.PolicyType.ALLOWLIST); @@ -89,5 +89,5 @@ token.seizeWithMemo(alice, treasury, amount, keccak256("court-order-123")); ``` -`AccountNotSeizable` selector: `0x91dbbc8d`. This error fires when the holder scope is unset or the holder is not on the attached blocklist — not when the holder is on a transfer blocklist. Transfer and seize scopes are independent. +`AccountNotSeizable` selector: `0x91dbbc8d`. This error fires when the holder scope is unset or the holder is not on the attached blocklist, not when the holder is on a transfer blocklist. Transfer and seize scopes are independent. diff --git a/docs/specifications/b20/reference/interfaces/ib20/set-role-admin.mdx b/docs/specifications/b20/reference/interfaces/ib20/set-role-admin.mdx index e91bafb72..952da7288 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/set-role-admin.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/set-role-admin.mdx @@ -29,13 +29,7 @@ Emits `RoleAdminChanged(role, previousAdminRole, newAdminRole)`. ## Reverts -- `AccessControlUnauthorizedAccount(account, neededRole)` — caller does not hold the current admin role for `role`, or the token has no admins (transitioned via `renounceLastAdmin`). - -## Access Control - -Caller must hold `getRoleAdmin(role)` at the time of the call. On a fresh token, every role's admin is `DEFAULT_ADMIN_ROLE`. - -Setting `newAdminRole` equal to `role` makes the role self-administrating. Combined with revoking all current holders, this permanently closes the ability to grant that role to new accounts. +- `AccessControlUnauthorizedAccount(account, neededRole)`: caller does not hold the current admin role for `role`, or the token has no admins (transitioned via `renounceLastAdmin`). ## Events @@ -43,6 +37,12 @@ Setting `newAdminRole` equal to `role` makes the role self-administrating. Combi |---|---| | `RoleAdminChanged(role, previousAdminRole, newAdminRole)` | Admin reassignment succeeds | +## Access Control + +Caller must hold `getRoleAdmin(role)` at the time of the call. On a fresh token, every role's admin is `DEFAULT_ADMIN_ROLE`. + +Setting `newAdminRole` equal to `role` makes the role self-administrating. Combined with revoking all current holders, this permanently closes the ability to grant that role to new accounts. + ## Example ```solidity Usage Example diff --git a/docs/specifications/b20/reference/interfaces/ib20/unpause.mdx b/docs/specifications/b20/reference/interfaces/ib20/unpause.mdx index 7cc5c3127..0d6beccd5 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/unpause.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/unpause.mdx @@ -3,8 +3,6 @@ title: "IB20.unpause" description: "Resumes one or more paused token operation classes on a B20 token." --- - - ## Signature ```solidity IB20.sol @@ -13,14 +11,14 @@ function unpause(PausableFeature[] calldata features) external; | Field | Value | |---|---| -| Selector | `0xb61b2ebc` | -| Canonical signature | `unpause(PausableFeature[])` | +| Selector | `0x8b93dd63` | +| Canonical signature | `unpause(uint8[])` | ## Description Resumes each feature listed in `features`. Features not listed remain in their current state. Duplicates in the array are a no-op. The call does not revert when a feature is already unpaused. -Emits `Unpaused(updater, features)` with the exact array passed. That array is not the resulting paused set — read `isPaused(feature)` or `pausedFeatures()` for the current state. +Emits `Unpaused(updater, features)` with the exact array passed. That array is not the resulting paused set, read `isPaused(feature)` or `pausedFeatures()` for the current state. ## Parameters @@ -28,19 +26,10 @@ Emits `Unpaused(updater, features)` with the exact array passed. That array is n |---|---| | `features` | One or more `PausableFeature` values to resume. Must be non-empty. | -## PausableFeature values - -| Value | Gates | -|---|---| -| `TRANSFER` | `transfer`, `transferFrom`, and memo'd variants | -| `MINT` | `mint`, `mintWithMemo`, and Asset `batchMint` | -| `BURN` | `burn`, `burnWithMemo`, and the deprecated `burnBlocked` | -| `SEIZE` | `seizeWithMemo` | - ## Reverts -- `AccessControlUnauthorizedAccount(account, neededRole)` — caller does not hold `UNPAUSE_ROLE`. -- `EmptyFeatureSet()` — `features` is an empty array. +- `AccessControlUnauthorizedAccount(account, neededRole)`: caller does not hold `UNPAUSE_ROLE`. +- `EmptyFeatureSet()`: `features` is an empty array. ## Access Control @@ -50,9 +39,20 @@ Emits `Unpaused(updater, features)` with the exact array passed. That array is n No direct policy interaction. +## Pausable Features + +| Value | Gates | +|---|---| +| `TRANSFER` | `transfer`, `transferFrom`, and memo'd variants | +| `MINT` | `mint`, `mintWithMemo`, and Asset `batchMint` | +| `BURN` | `burn`, `burnWithMemo`, and the deprecated `burnBlocked` | +| `SEIZE` | `seizeWithMemo` | + ## Example ```solidity Usage Example // Unpause BURN only; MINT remains paused if it was paused separately. -IB20(target).unpause([PausableFeature.BURN]); +PausableFeature[] memory features = new PausableFeature[](1); +features[0] = PausableFeature.BURN; +IB20(target).unpause(features); ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20/update-policy.mdx b/docs/specifications/b20/reference/interfaces/ib20/update-policy.mdx index f2872fbfc..545b356d4 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/update-policy.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/update-policy.mdx @@ -49,10 +49,10 @@ The recognized scopes and the accounts they check are: | `TRANSFER_RECEIVER_POLICY` | `to` | `false` | | `TRANSFER_EXECUTOR_POLICY` | `msg.sender` (on `transferFrom` when `msg.sender != from`) | `false` | | `MINT_RECEIVER_POLICY` | `to` | `false` | -| `SEIZE_HOLDER_POLICY` | `from` | `true` | +| `SEIZE_EXEMPT_POLICY` | `from` | `true` | | `SEIZE_RECEIVER_POLICY` | `to` | `false` | -Transfer scopes are skipped on factory `initCalls` transfers. `MINT_RECEIVER_POLICY` is always checked, including factory `initCalls` mints. `SEIZE_HOLDER_POLICY` unset (`ALWAYS_ALLOW`) means no account is seizable. +Transfer scopes are skipped on factory `initCalls` transfers. `MINT_RECEIVER_POLICY` is always checked, including factory `initCalls` mints. `SEIZE_EXEMPT_POLICY` unset (`ALWAYS_ALLOW`) means no account is seizable. ## Example @@ -60,4 +60,4 @@ Transfer scopes are skipped on factory `initCalls` transfers. `MINT_RECEIVER_POL IB20(token).updatePolicy(B20Constants.TRANSFER_RECEIVER_POLICY, policyId); ``` -The same policy ID can be bound to more than one scope and to more than one token. Updating membership in the registry policy is visible immediately on every scope and token that references it — no second `updatePolicy` call is needed. +The same policy ID can be bound to more than one scope and to more than one token. Updating membership in the registry policy is visible immediately on every scope and token that references it, no second `updatePolicy` call is needed. diff --git a/docs/specifications/b20/specification-overview.mdx b/docs/specifications/b20/specification-overview.mdx index ae44d7610..968614122 100644 --- a/docs/specifications/b20/specification-overview.mdx +++ b/docs/specifications/b20/specification-overview.mdx @@ -101,7 +101,7 @@ B20 tokens store one `uint64 policyId` per supported policy scope. | `TRANSFER_RECEIVER_POLICY` | `to` | `transfer`, `transferFrom`, and memo variants | | `TRANSFER_EXECUTOR_POLICY` | `msg.sender` | `transferFrom` when `msg.sender != from` | | `MINT_RECEIVER_POLICY` | `to` | `mint`, `mintWithMemo` | -| `SEIZE_HOLDER_POLICY` | `from` | `seizeWithMemo`; holder is seizable only when not authorized | +| `SEIZE_EXEMPT_POLICY` | `from` | `seizeWithMemo`; an authorized holder is seize-exempt, so only unauthorized holders are seizable | | `SEIZE_RECEIVER_POLICY` | `to` | `seizeWithMemo`; destination must be authorized | All scopes default to `ALWAYS_ALLOW` at creation. `approve` and `permit` are not policy-gated. @@ -118,7 +118,7 @@ The legacy `burnBlocked` path is deprecated and retained for backwards compatibi ## Seize -`seizeWithMemo(from, to, amount, memo)` transfers balance from `from` to `to` and emits `Transfer`, `Memo`, and `Seized`. It is gated by `SEIZE_ROLE`, skips allowance and transfer policies, requires `from` to be denied by `SEIZE_HOLDER_POLICY`, and requires `to` to be authorized by `SEIZE_RECEIVER_POLICY`. +`seizeWithMemo(from, to, amount, memo)` transfers balance from `from` to `to` and emits `Transfer`, `Memo`, and `Seized`. It is gated by `SEIZE_ROLE`, skips allowance and transfer policies, requires `from` to be denied by `SEIZE_EXEMPT_POLICY`, and requires `to` to be authorized by `SEIZE_RECEIVER_POLICY`. ## Supply Cap diff --git a/examples/verified-doc-samples/cli/b20-operations.sh b/examples/verified-doc-samples/cli/b20-operations.sh index c203bcbf0..5ad6ca161 100755 --- a/examples/verified-doc-samples/cli/b20-operations.sh +++ b/examples/verified-doc-samples/cli/b20-operations.sh @@ -87,9 +87,10 @@ base-cast call "$TOKEN_ADDRESS" "balanceOf(address)(uint256)" "$BLOCKED_HOLDER" # docs:end stock-cancel-cli # docs:start stock-split-cli -base-cast send "$TOKEN_ADDRESS" "updateMultiplier(uint256)" 2000000000000000000 \ +EFFECTIVE_AT=$(( $(date +%s) + 86400 )) +base-cast send "$TOKEN_ADDRESS" "updateUIMultiplier(uint256,uint256)" 2000000000000000000 "$EFFECTIVE_AT" \ --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" -base-cast call "$TOKEN_ADDRESS" "scaledBalanceOf(address)(uint256)" "$HOLDER" --rpc-url "$RPC_URL" +base-cast call "$TOKEN_ADDRESS" "newUIMultiplier()(uint256)" --rpc-url "$RPC_URL" # docs:end stock-split-cli # docs:start stock-pause-cli diff --git a/examples/verified-doc-samples/solidity/script/B20Examples.s.sol b/examples/verified-doc-samples/solidity/script/B20Examples.s.sol index 1794d2ac8..18b138814 100644 --- a/examples/verified-doc-samples/solidity/script/B20Examples.s.sol +++ b/examples/verified-doc-samples/solidity/script/B20Examples.s.sol @@ -142,10 +142,11 @@ contract B20Examples is Script { // docs:end stock-dividend-solidity // docs:start stock-split-solidity - function splitStock(address token, address holder) public returns (uint256 scaledBalance) { - IB20Asset(token).updateMultiplier(2e18); - require(IB20Asset(token).multiplier() == 2e18, "multiplier not updated"); - scaledBalance = IB20Asset(token).scaledBalanceOf(holder); + function scheduleSplit(address token) public returns (uint256 effectiveAt) { + effectiveAt = block.timestamp + 1 days; + IB20Asset(token).updateUIMultiplier(2e18, effectiveAt); + require(IB20Asset(token).newUIMultiplier() == 2e18, "split not scheduled"); + require(IB20Asset(token).effectiveAt() == effectiveAt, "effectiveAt mismatch"); } // docs:end stock-split-solidity } diff --git a/examples/verified-doc-samples/typescript/src/b20/abi.ts b/examples/verified-doc-samples/typescript/src/b20/abi.ts index dc64bd7a6..961a99152 100644 --- a/examples/verified-doc-samples/typescript/src/b20/abi.ts +++ b/examples/verified-doc-samples/typescript/src/b20/abi.ts @@ -31,8 +31,14 @@ const assetExtraAbi = parseAbi([ "function announce(bytes[],string,string,string)", "function isAnnouncementIdUsed(string) view returns (bool)", "function updateMultiplier(uint256)", + "function updateUIMultiplier(uint256,uint256)", + "function cancelUIMultiplierUpdate()", "function multiplier() view returns (uint256)", + "function uiMultiplier() view returns (uint256)", + "function newUIMultiplier() view returns (uint256)", + "function effectiveAt() view returns (uint256)", "function scaledBalanceOf(address) view returns (uint256)", + "function balanceOfUI(address) view returns (uint256)", ]); export const assetAbi = [...b20Abi, ...assetExtraAbi] as const; diff --git a/examples/verified-doc-samples/typescript/src/b20/stock/stock-split-ts.ts b/examples/verified-doc-samples/typescript/src/b20/stock/stock-split-ts.ts index 60288da41..f2ee6a31b 100644 --- a/examples/verified-doc-samples/typescript/src/b20/stock/stock-split-ts.ts +++ b/examples/verified-doc-samples/typescript/src/b20/stock/stock-split-ts.ts @@ -4,11 +4,11 @@ import { publicClient } from "../../shared/clients.js"; import { assetAbi } from "../abi.js"; import { sendContract } from "../write.js"; -export async function runTwoForOneSplit(token: Address, holder: Address) { - await sendContract({ address: token, abi: assetAbi, functionName: "updateMultiplier", args: [2n * 10n ** 18n] }); - const multiplier = await publicClient.readContract({ address: token, abi: assetAbi, functionName: "multiplier" }); - const scaled = await publicClient.readContract({ address: token, abi: assetAbi, functionName: "scaledBalanceOf", args: [holder] }); - if (multiplier !== 2n * 10n ** 18n) throw new Error("Multiplier was not updated"); - return scaled; +export async function scheduleTwoForOneSplit(token: Address) { + const effectiveAt = BigInt(Math.floor(Date.now() / 1000) + 86_400); + await sendContract({ address: token, abi: assetAbi, functionName: "updateUIMultiplier", args: [2n * 10n ** 18n, effectiveAt] }); + const pending = await publicClient.readContract({ address: token, abi: assetAbi, functionName: "newUIMultiplier" }); + if (pending !== 2n * 10n ** 18n) throw new Error("Split was not scheduled"); + return effectiveAt; } // docs:end stock-split-ts diff --git a/examples/verified-doc-samples/typescript/src/live/b20.ts b/examples/verified-doc-samples/typescript/src/live/b20.ts index 3963b5e56..e5a477e20 100644 --- a/examples/verified-doc-samples/typescript/src/live/b20.ts +++ b/examples/verified-doc-samples/typescript/src/live/b20.ts @@ -16,7 +16,7 @@ import { issueShares } from "../b20/stock/stock-issue-ts.js"; import { restrictStockHolders } from "../b20/stock/stock-restrict-ts.js"; import { cancelBlockedShares } from "../b20/stock/stock-cancel-ts.js"; import { announceStockDividend } from "../b20/stock/stock-dividend-ts.js"; -import { runTwoForOneSplit } from "../b20/stock/stock-split-ts.js"; +import { scheduleTwoForOneSplit } from "../b20/stock/stock-split-ts.js"; import { setStockTransfersPaused } from "../b20/stock/stock-pause-ts.js"; const holder = required("HOLDER_ADDRESS") as Address; @@ -70,7 +70,7 @@ async function main() { await setBlocked(stockBlocklist, holder, true); await cancelBlockedShares(stock, holder); await announceStockDividend(stock, [account.address, holder]); - await runTwoForOneSplit(stock, account.address); + await scheduleTwoForOneSplit(stock); await setStockTransfersPaused(stock, true); await setStockTransfersPaused(stock, false); diff --git a/scripts/sync-from-base-std/route-table.json b/scripts/sync-from-base-std/route-table.json index a8aebdd15..64917ffaf 100644 --- a/scripts/sync-from-base-std/route-table.json +++ b/scripts/sync-from-base-std/route-table.json @@ -310,7 +310,7 @@ "docs/build-on-base/issue-stablecoins/recover-funds.mdx", "docs/specifications/b20/reference/interfaces/ib20/seize-with-memo.mdx", "docs/specifications/b20/reference/interfaces/ib20/seize-role.mdx", - "docs/specifications/b20/reference/interfaces/ib20/seize-holder-policy.mdx", + "docs/specifications/b20/reference/interfaces/ib20/seize-exempt-policy.mdx", "docs/specifications/b20/reference/interfaces/ib20/seize-receiver-policy.mdx" ], "transformer": "claude" diff --git a/scripts/verify-doc-samples.sh b/scripts/verify-doc-samples.sh index 5a99fe000..93662e7c9 100755 --- a/scripts/verify-doc-samples.sh +++ b/scripts/verify-doc-samples.sh @@ -44,7 +44,9 @@ bash -n "$ROOT/examples/verified-doc-samples/cli/b20-operations.sh" exit 127 fi - if [[ ! -d lib/base-std ]]; then "$forge_cmd" install base/base-std@v1.0.0 --no-git; fi + # Pinned to a base-std main commit rather than a Beryl tag: the multiplier + # scheduling and seize samples need the Cobalt interface surface. + if [[ ! -d lib/base-std ]]; then "$forge_cmd" install base/base-std@be6d0450890e20fc4a739aeaff5e839f234d12a6 --no-git; fi if [[ ! -d lib/forge-std ]]; then "$forge_cmd" install foundry-rs/forge-std@v1.9.7 --no-git; fi "$forge_cmd" build ) From e553cd959a7fc5f07118513b314e6e135c749515 Mon Sep 17 00:00:00 2001 From: Soheima M Date: Tue, 8 Sep 2026 18:33:48 +0200 Subject: [PATCH 3/4] docs(b20): drop the literal Title keyword from fences, align sample pin notes Mintlify renders the whole fence meta as the block title, so "Title Read the pending state" showed the word Title. Use the bare title like the rest of the reference pages. Every Build on Base guide note now names the base-std commit the verified samples compile against. --- docs/build-on-base/issue-rwa/announce-a-distribution.mdx | 8 ++++---- docs/build-on-base/issue-rwa/apply-a-multiplier.mdx | 6 +++--- docs/build-on-base/issue-rwa/cancel-blocked-units.mdx | 2 +- docs/build-on-base/issue-rwa/create-an-asset-token.mdx | 2 +- docs/build-on-base/issue-rwa/issue-units.mdx | 2 +- docs/build-on-base/issue-rwa/pause-transfers.mdx | 2 +- .../build-on-base/issue-rwa/restrict-eligible-holders.mdx | 2 +- docs/build-on-base/issue-stablecoins/block-an-account.mdx | 2 +- docs/build-on-base/issue-stablecoins/burn-supply.mdx | 2 +- .../issue-stablecoins/issue-your-stablecoin.mdx | 2 +- docs/build-on-base/issue-stablecoins/mint-supply.mdx | 2 +- docs/build-on-base/issue-stablecoins/pause-activity.mdx | 2 +- .../issue-stablecoins/reconcile-with-memos.mdx | 2 +- docs/build-on-base/issue-stablecoins/recover-funds.mdx | 2 +- .../issue-stablecoins/restrict-who-can-hold.mdx | 2 +- .../i-policy-registry/create-composite-policy.mdx | 2 +- .../interfaces/i-policy-registry/stage-update-admin.mdx | 2 +- .../interfaces/i-policy-registry/update-allowlist.mdx | 4 ++-- .../interfaces/i-policy-registry/update-blocklist.mdx | 2 +- .../b20/reference/interfaces/ib20-asset/announce.mdx | 4 ++-- .../b20/reference/interfaces/ib20-asset/batch-mint.mdx | 4 ++-- .../b20/reference/interfaces/ib20-asset/ui-multiplier.mdx | 2 +- .../b20/reference/interfaces/ib20/seize-exempt-policy.mdx | 2 +- .../reference/interfaces/ib20/seize-receiver-policy.mdx | 2 +- .../b20/reference/interfaces/ib20/seize-role.mdx | 2 +- 25 files changed, 33 insertions(+), 33 deletions(-) diff --git a/docs/build-on-base/issue-rwa/announce-a-distribution.mdx b/docs/build-on-base/issue-rwa/announce-a-distribution.mdx index b9fe8d3d9..1440a6c83 100644 --- a/docs/build-on-base/issue-rwa/announce-a-distribution.mdx +++ b/docs/build-on-base/issue-rwa/announce-a-distribution.mdx @@ -96,7 +96,7 @@ The same bracket discloses any operator-driven change. Each scenario below lists Wrap `updateUIMultiplier(newMultiplier, effectiveAt)`. A 2-for-1 split uses `2e18`. A reverse split uses a value below `1e18`. The operator needs `OPERATOR_ROLE` only. -```solidity Title Announce a scheduled split lines wrap expandable highlight={3} +```solidity Announce a scheduled split lines wrap expandable highlight={3} function announceSplit(address token, uint256 newMultiplier, uint256 effectiveAt) public { bytes[] memory calls = new bytes[](1); calls[0] = abi.encodeCall(IB20Asset.updateUIMultiplier, (newMultiplier, effectiveAt)); @@ -113,7 +113,7 @@ On success the asset emits `Announcement`, `UIMultiplierUpdated`, then `EndAnnou To replace a live pending update, cancel and reschedule in one `announce`: -```solidity Title Cancel and reschedule in one announcement wrap +```solidity Cancel and reschedule in one announcement wrap bytes[] memory calls = new bytes[](2); calls[0] = abi.encodeCall(IB20Asset.cancelUIMultiplierUpdate, ()); calls[1] = abi.encodeCall(IB20Asset.updateUIMultiplier, (newMultiplier, effectiveAt)); @@ -124,7 +124,7 @@ IB20Asset(token).announce(calls, id, description, uri); Wrap `burnWithMemo(amount, memo)`. The call burns the operator's own balance. The operator needs `OPERATOR_ROLE` and `BURN_ROLE`. `BURN` must not be paused. -```solidity Title Announce a treasury burn wrap +```solidity Announce a treasury burn wrap bytes[] memory calls = new bytes[](1); calls[0] = abi.encodeCall(IB20Asset.burnWithMemo, (amount, memo)); IB20Asset(token).announce(calls, id, "Treasury burn", uri); @@ -136,7 +136,7 @@ On success the asset emits `Announcement`, `Transfer(operator, address(0), amoun Pass an empty `internalCalls` array. The operator needs `OPERATOR_ROLE` only. The asset emits `Announcement` then `EndAnnouncement` with nothing between them. The `id` is still consumed. -```solidity Title Notice only +```solidity Notice only IB20Asset(token).announce(new bytes[](0), id, description, uri); ``` diff --git a/docs/build-on-base/issue-rwa/apply-a-multiplier.mdx b/docs/build-on-base/issue-rwa/apply-a-multiplier.mdx index 1156970d6..9653da8d6 100644 --- a/docs/build-on-base/issue-rwa/apply-a-multiplier.mdx +++ b/docs/build-on-base/issue-rwa/apply-a-multiplier.mdx @@ -82,7 +82,7 @@ On success, the asset emits `UIMultiplierUpdated(oldMultiplier, newMultiplier, e ### Read the Live Pending State -```solidity Title Read the pending state +```solidity Read the pending state asset.uiMultiplier(); // still the current multiplier asset.newUIMultiplier(); // scheduled target asset.effectiveAt(); // flip timestamp @@ -102,7 +102,7 @@ When `block.timestamp >= effectiveAt`, `uiMultiplier()` returns the new multipli Call before `effectiveAt` to discard the pending split: -```solidity Title Cancel the pending update +```solidity Cancel the pending update asset.cancelUIMultiplierUpdate(); ``` @@ -110,7 +110,7 @@ Emits `UIMultiplierUpdateCancelled(cancelledMultiplier, cancelledEffectiveAt)`. To cancel and reschedule atomically, wrap both calls in `announce`: -```solidity Title Cancel and reschedule in one announcement wrap +```solidity Cancel and reschedule in one announcement wrap bytes[] memory calls = new bytes[](2); calls[0] = abi.encodeCall(IB20Asset.cancelUIMultiplierUpdate, ()); calls[1] = abi.encodeCall(IB20Asset.updateUIMultiplier, (secondMultiplier, secondEffectiveAt)); diff --git a/docs/build-on-base/issue-rwa/cancel-blocked-units.mdx b/docs/build-on-base/issue-rwa/cancel-blocked-units.mdx index e83bcbfeb..aa5eb292e 100644 --- a/docs/build-on-base/issue-rwa/cancel-blocked-units.mdx +++ b/docs/build-on-base/issue-rwa/cancel-blocked-units.mdx @@ -20,7 +20,7 @@ The demo uses a local browser-generated account to submit real transactions on * -New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@v1.0.0`, `viem@2.55.11`, and Base Foundry `v1.1.1`. +New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Cancel and Verify Blocked Shares diff --git a/docs/build-on-base/issue-rwa/create-an-asset-token.mdx b/docs/build-on-base/issue-rwa/create-an-asset-token.mdx index 3c1701511..d7054f39a 100644 --- a/docs/build-on-base/issue-rwa/create-an-asset-token.mdx +++ b/docs/build-on-base/issue-rwa/create-an-asset-token.mdx @@ -20,7 +20,7 @@ The demo uses a local browser-generated account to submit real transactions on * -New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@v1.0.0`, `viem@2.55.11`, and Base Foundry `v1.1.1`. +New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Create and Verify the Stock Token diff --git a/docs/build-on-base/issue-rwa/issue-units.mdx b/docs/build-on-base/issue-rwa/issue-units.mdx index 9241081f7..cebda9882 100644 --- a/docs/build-on-base/issue-rwa/issue-units.mdx +++ b/docs/build-on-base/issue-rwa/issue-units.mdx @@ -20,7 +20,7 @@ The demo uses a local browser-generated account to submit real transactions on * -New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@v1.0.0`, `viem@2.55.11`, and Base Foundry `v1.1.1`. +New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Issue and Verify the Shares diff --git a/docs/build-on-base/issue-rwa/pause-transfers.mdx b/docs/build-on-base/issue-rwa/pause-transfers.mdx index f9c522d44..4591c73c9 100644 --- a/docs/build-on-base/issue-rwa/pause-transfers.mdx +++ b/docs/build-on-base/issue-rwa/pause-transfers.mdx @@ -20,7 +20,7 @@ The demo uses a local browser-generated account to submit real transactions on * -New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@v1.0.0`, `viem@2.55.11`, and Base Foundry `v1.1.1`. +New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Pause and Resume Stock Transfers diff --git a/docs/build-on-base/issue-rwa/restrict-eligible-holders.mdx b/docs/build-on-base/issue-rwa/restrict-eligible-holders.mdx index 7cc9a82b1..07f73ef89 100644 --- a/docs/build-on-base/issue-rwa/restrict-eligible-holders.mdx +++ b/docs/build-on-base/issue-rwa/restrict-eligible-holders.mdx @@ -20,7 +20,7 @@ The demo uses a local browser-generated account to submit real transactions on * -New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@v1.0.0`, `viem@2.55.11`, and Base Foundry `v1.1.1`. +New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Create and Bind the Eligibility Policy diff --git a/docs/build-on-base/issue-stablecoins/block-an-account.mdx b/docs/build-on-base/issue-stablecoins/block-an-account.mdx index d5e27684b..3d8fca74e 100644 --- a/docs/build-on-base/issue-stablecoins/block-an-account.mdx +++ b/docs/build-on-base/issue-stablecoins/block-an-account.mdx @@ -17,7 +17,7 @@ The demo uses a local browser-generated account to submit real transactions on * -New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@v1.0.0`, `viem@2.55.11`, and Base Foundry `v1.1.1`. +New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Block and Verify an Account diff --git a/docs/build-on-base/issue-stablecoins/burn-supply.mdx b/docs/build-on-base/issue-stablecoins/burn-supply.mdx index beafaff0b..95194a78e 100644 --- a/docs/build-on-base/issue-stablecoins/burn-supply.mdx +++ b/docs/build-on-base/issue-stablecoins/burn-supply.mdx @@ -17,7 +17,7 @@ The demo uses a local browser-generated account to submit real transactions on * -New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@v1.0.0`, `viem@2.55.11`, and Base Foundry `v1.1.1`. +New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Burn and Verify Supply diff --git a/docs/build-on-base/issue-stablecoins/issue-your-stablecoin.mdx b/docs/build-on-base/issue-stablecoins/issue-your-stablecoin.mdx index f5c52842d..c48852706 100644 --- a/docs/build-on-base/issue-stablecoins/issue-your-stablecoin.mdx +++ b/docs/build-on-base/issue-stablecoins/issue-your-stablecoin.mdx @@ -17,7 +17,7 @@ The demo uses a local browser-generated account to submit real transactions on * -New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@v1.0.0`, `viem@2.55.11`, and Base Foundry `v1.1.1`. +New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## How the Stablecoin Variant Works diff --git a/docs/build-on-base/issue-stablecoins/mint-supply.mdx b/docs/build-on-base/issue-stablecoins/mint-supply.mdx index ad06d98f6..42ae0a9a1 100644 --- a/docs/build-on-base/issue-stablecoins/mint-supply.mdx +++ b/docs/build-on-base/issue-stablecoins/mint-supply.mdx @@ -17,7 +17,7 @@ The demo uses a local browser-generated account to submit real transactions on * -New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@v1.0.0`, `viem@2.55.11`, and Base Foundry `v1.1.1`. +New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Mint and Verify Supply diff --git a/docs/build-on-base/issue-stablecoins/pause-activity.mdx b/docs/build-on-base/issue-stablecoins/pause-activity.mdx index f2a7c58d9..0695effab 100644 --- a/docs/build-on-base/issue-stablecoins/pause-activity.mdx +++ b/docs/build-on-base/issue-stablecoins/pause-activity.mdx @@ -19,7 +19,7 @@ The demo uses a local browser-generated account to submit real transactions on * -New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@v1.0.0`, `viem@2.55.11`, and Base Foundry `v1.1.1`. +New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Pause and Resume Transfers diff --git a/docs/build-on-base/issue-stablecoins/reconcile-with-memos.mdx b/docs/build-on-base/issue-stablecoins/reconcile-with-memos.mdx index 7045913e8..739e39553 100644 --- a/docs/build-on-base/issue-stablecoins/reconcile-with-memos.mdx +++ b/docs/build-on-base/issue-stablecoins/reconcile-with-memos.mdx @@ -19,7 +19,7 @@ The demo uses a local browser-generated account to submit real transactions on * -New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@v1.0.0`, `viem@2.55.11`, and Base Foundry `v1.1.1`. +New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Attach and Read an Invoice Memo diff --git a/docs/build-on-base/issue-stablecoins/recover-funds.mdx b/docs/build-on-base/issue-stablecoins/recover-funds.mdx index 450046d24..1a1e662d1 100644 --- a/docs/build-on-base/issue-stablecoins/recover-funds.mdx +++ b/docs/build-on-base/issue-stablecoins/recover-funds.mdx @@ -17,7 +17,7 @@ The demo uses a local browser-generated account to submit real transactions on * -New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@v1.0.0`, `viem@2.55.11`, and Base Foundry `v1.1.1`. +New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Burn and Reissue the Blocked Balance diff --git a/docs/build-on-base/issue-stablecoins/restrict-who-can-hold.mdx b/docs/build-on-base/issue-stablecoins/restrict-who-can-hold.mdx index 2ddef2679..9e1a67a3a 100644 --- a/docs/build-on-base/issue-stablecoins/restrict-who-can-hold.mdx +++ b/docs/build-on-base/issue-stablecoins/restrict-who-can-hold.mdx @@ -17,7 +17,7 @@ The demo uses a local browser-generated account to submit real transactions on * -New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@v1.0.0`, `viem@2.55.11`, and Base Foundry `v1.1.1`. +New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## How Policies Work diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/create-composite-policy.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/create-composite-policy.mdx index 4c530bbc1..d54cd7a0c 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/create-composite-policy.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/create-composite-policy.mdx @@ -60,7 +60,7 @@ Permissionless, any caller may create a composite policy. ## Example -```solidity Title Create a KYC-and-sanctions composite lines wrap expandable highlight={10} +```solidity Create a KYC-and-sanctions composite lines wrap expandable highlight={10} // 1. Create a KYC allowlist and a sanctions blocklist first. uint64 kycId = registry.createPolicy(admin, PolicyType.ALLOWLIST); uint64 sanctionsId = registry.createPolicy(admin, PolicyType.BLOCKLIST); diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/stage-update-admin.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/stage-update-admin.mdx index 991f0ee2f..1a96345cd 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/stage-update-admin.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/stage-update-admin.mdx @@ -44,7 +44,7 @@ Callable only by the current policy admin for `policyId`. Any other caller rever ## Example -```solidity Title Admin handoff +```solidity Admin handoff // Step 1: current admin nominates a successor IPolicyRegistry(registry).stageUpdateAdmin(policyId, nextAdmin); diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/update-allowlist.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/update-allowlist.mdx index b288c69ea..11cf1e270 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/update-allowlist.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/update-allowlist.mdx @@ -47,7 +47,7 @@ Callable only by the current admin of `policyId`. Any other caller reverts `Unau ## Example -```solidity Title Add accounts to an allowlist +```solidity Add accounts to an allowlist address[] memory accounts = new address[](2); accounts[0] = alice; accounts[1] = bob; @@ -58,7 +58,7 @@ After this call, `isAuthorized(kycId, alice)` and `isAuthorized(kycId, bob)` ret To remove an account, pass `false`: -```solidity Title Remove an account from an allowlist +```solidity Remove an account from an allowlist address[] memory removed = new address[](1); removed[0] = alice; IPolicyRegistry(registry).updateAllowlist(kycId, false, removed); diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/update-blocklist.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/update-blocklist.mdx index 852f7877e..fe0e45ac1 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/update-blocklist.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/update-blocklist.mdx @@ -47,7 +47,7 @@ Callable only by the current admin of `policyId`. Any other caller reverts `Unau ## Example -```solidity Title Add accounts to a blocklist lines wrap expandable highlight={5,10} +```solidity Add accounts to a blocklist lines wrap expandable highlight={5,10} // Add two addresses to an existing BLOCKLIST policy. address[] memory blocked = new address[](2); blocked[0] = carol; diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/announce.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/announce.mdx index 81c379d24..c4d1926f7 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/announce.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/announce.mdx @@ -57,7 +57,7 @@ A Solidity `Panic` inside an inner call (for example arithmetic overflow) propag ## Example -```solidity Title Scheduled split wrapped in an announcement +```solidity Scheduled split wrapped in an announcement bytes[] memory calls = new bytes[](1); calls[0] = abi.encodeCall(IB20Asset.updateUIMultiplier, (2e18, effectiveAt)); @@ -65,7 +65,7 @@ asset.announce(calls, id, description, uri); // Emits: Announcement → UIMultiplierUpdated → EndAnnouncement ``` -```solidity Title Notice with no inner calls +```solidity Notice with no inner calls asset.announce(new bytes[](0), id, description, uri); // Emits: Announcement → EndAnnouncement ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/batch-mint.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/batch-mint.mdx index db49e3008..5e8c99658 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/batch-mint.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/batch-mint.mdx @@ -47,7 +47,7 @@ Each recipient must pass `MINT_RECEIVER_POLICY`. Reverts `PolicyForbids(MINT_REC ## Example -```solidity Title Usage Example lines wrap expandable highlight={9} +```solidity Usage Example lines wrap expandable highlight={9} address[] memory recipients = new address[](2); recipients[0] = holderA; recipients[1] = holderB; @@ -61,7 +61,7 @@ IB20Asset(asset).batchMint(recipients, amounts); Use `batchMint` inside an `announce` call to disclose a dividend issuance or additional mint as a corporate action: -```solidity Title Announced Batch Mint +```solidity Announced Batch Mint bytes[] memory calls = new bytes[](1); calls[0] = abi.encodeCall(IB20Asset.batchMint, (recipients, amounts)); asset.announce(calls, id, description, uri); diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/ui-multiplier.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/ui-multiplier.mdx index ac3117131..ace304812 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/ui-multiplier.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/ui-multiplier.mdx @@ -20,7 +20,7 @@ Returns the effective UI multiplier at `block.timestamp`, represented with 18 de Raw ERC-20 balances are never rewritten during a split. The multiplier changes the displayed scale only. A wallet or indexer derives the UI share count as: -```text Title UI balance formula +```text UI balance formula ui = balanceOf(account) * uiMultiplier() / 1e18 ``` diff --git a/docs/specifications/b20/reference/interfaces/ib20/seize-exempt-policy.mdx b/docs/specifications/b20/reference/interfaces/ib20/seize-exempt-policy.mdx index e3d8e7f90..46b91ad36 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/seize-exempt-policy.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/seize-exempt-policy.mdx @@ -43,7 +43,7 @@ This scope is independent of `TRANSFER_SENDER_POLICY`. Blocking a holder from tr ## Example -```solidity Title Make one holder seizable lines wrap expandable +```solidity Make one holder seizable lines wrap expandable // Create a blocklist and add the holder uint64 seizableId = registry.createPolicy(policyAdmin, IPolicyRegistry.PolicyType.BLOCKLIST); address[] memory holders = new address[](1); diff --git a/docs/specifications/b20/reference/interfaces/ib20/seize-receiver-policy.mdx b/docs/specifications/b20/reference/interfaces/ib20/seize-receiver-policy.mdx index 6512f334c..04d880581 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/seize-receiver-policy.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/seize-receiver-policy.mdx @@ -38,7 +38,7 @@ Attach a policy ID to this scope with `updatePolicy(token.SEIZE_RECEIVER_POLICY( ## Example -```solidity Title Restrict seize destinations to a treasury lines wrap expandable highlight={11} +```solidity Restrict seize destinations to a treasury lines wrap expandable highlight={11} // Read the scope key bytes32 scope = IB20(token).SEIZE_RECEIVER_POLICY(); diff --git a/docs/specifications/b20/reference/interfaces/ib20/seize-role.mdx b/docs/specifications/b20/reference/interfaces/ib20/seize-role.mdx index 439ed61e1..7f496c03e 100644 --- a/docs/specifications/b20/reference/interfaces/ib20/seize-role.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20/seize-role.mdx @@ -34,7 +34,7 @@ No direct policy interaction. The role returned here is used alongside `SEIZE_EX Grant this role to the account that will call `seizeWithMemo`: -```solidity Title Grant SEIZE_ROLE and seize +```solidity Grant SEIZE_ROLE and seize bytes32 seizeRole = token.SEIZE_ROLE(); token.grantRole(seizeRole, seizer); From 227c0bbb979c5ed6cb1d740eff63a9d9679fd51f Mon Sep 17 00:00:00 2001 From: Soheima M Date: Tue, 8 Sep 2026 19:23:56 +0200 Subject: [PATCH 4/4] docs(b20): drop the stock-split card and corporate-action wording Remove the "Run a Stock Split" See Also card from Announce a Distribution. Describe announcements and multiplier updates in those terms instead of "corporate actions" across the two guides and the announce, OPERATOR_ROLE, batchMint, and updateMultiplier reference pages. Example URIs use an announcements path; the dividend fixture is updated to match. --- .../issue-rwa/announce-a-distribution.mdx | 25 ++++++++----------- .../issue-rwa/apply-a-multiplier.mdx | 2 +- .../interfaces/ib20-asset/announce.mdx | 2 +- .../interfaces/ib20-asset/batch-mint.mdx | 2 +- .../interfaces/ib20-asset/operator-role.mdx | 4 +-- .../ib20-asset/update-multiplier.mdx | 2 +- .../solidity/script/B20Examples.s.sol | 2 +- 7 files changed, 18 insertions(+), 21 deletions(-) diff --git a/docs/build-on-base/issue-rwa/announce-a-distribution.mdx b/docs/build-on-base/issue-rwa/announce-a-distribution.mdx index 1440a6c83..5e564b4dd 100644 --- a/docs/build-on-base/issue-rwa/announce-a-distribution.mdx +++ b/docs/build-on-base/issue-rwa/announce-a-distribution.mdx @@ -1,13 +1,13 @@ --- title: "Announce a Distribution" description: "Wrap a holder-impacting action with an onchain disclosure using the B20 Asset announce function." -keywords: ["announce distribution", "B20 announcement", "asset distribution", "batchMint distribution", "stock split", "treasury burn"] +keywords: ["announce distribution", "B20 announcement", "asset distribution", "batchMint distribution", "multiplier update", "treasury burn"] --- import { AssetDemo } from "/snippets/AssetDemo.jsx" import RwaDisclaimer from "/snippets/RwaDisclaimer.mdx" -Bracket a holder-impacting action with an onchain description and supporting URI. `announce` emits `Announcement` before the inner calls and `EndAnnouncement` after, giving indexers a reliable bracket for every corporate action. +Bracket a holder-impacting action with an onchain description and supporting URI. `announce` emits `Announcement` before the inner calls and `EndAnnouncement` after, giving indexers a reliable bracket for every announced change. @@ -71,7 +71,7 @@ export async function announceStockDividend(token: Address, holders: Address[]) calls, "2026-stock-dividend-01", "Five-percent stock dividend", - "https://example.com/corporate-actions/2026-01" + "https://example.com/announcements/2026-01" ); require(IB20Asset(token).isAnnouncementIdUsed("2026-stock-dividend-01"), "announcement missing"); } @@ -85,26 +85,26 @@ On success, the unique announcement `id` is marked used and the batch mint execu -This example issues additional shares. It does not distribute a cash dividend. A reinvested dividend that only rescales displayed balances is a multiplier update, covered below. +This example issues additional shares. It does not distribute a cash dividend. A reinvested dividend that only rescales displayed balances is a multiplier update, covered below and in [Apply a Multiplier](/build-on-base/issue-rwa/apply-a-multiplier). -## Wrap Other Corporate Actions +## Announce Other Changes The same bracket discloses any operator-driven change. Each scenario below lists the roles the operator needs and the events the asset emits. -### Stock Split or Reverse Split +### Multiplier Update Wrap `updateUIMultiplier(newMultiplier, effectiveAt)`. A 2-for-1 split uses `2e18`. A reverse split uses a value below `1e18`. The operator needs `OPERATOR_ROLE` only. -```solidity Announce a scheduled split lines wrap expandable highlight={3} -function announceSplit(address token, uint256 newMultiplier, uint256 effectiveAt) public { +```solidity Announce a multiplier update lines wrap expandable highlight={3} +function announceMultiplierUpdate(address token, uint256 newMultiplier, uint256 effectiveAt) public { bytes[] memory calls = new bytes[](1); calls[0] = abi.encodeCall(IB20Asset.updateUIMultiplier, (newMultiplier, effectiveAt)); IB20Asset(token).announce( calls, - "2026-split-01", - "2-for-1 stock split", - "https://example.com/corporate-actions/2026-split-01" + "2026-multiplier-01", + "2-for-1 multiplier update", + "https://example.com/announcements/2026-multiplier-01" ); } ``` @@ -158,7 +158,4 @@ Typical inner causes of `InternalCallFailed`: missing `MINT_ROLE` or `BURN_ROLE` Mint asset units to holders. - - Apply a UI multiplier for stock splits. - diff --git a/docs/build-on-base/issue-rwa/apply-a-multiplier.mdx b/docs/build-on-base/issue-rwa/apply-a-multiplier.mdx index 9653da8d6..319d28047 100644 --- a/docs/build-on-base/issue-rwa/apply-a-multiplier.mdx +++ b/docs/build-on-base/issue-rwa/apply-a-multiplier.mdx @@ -114,7 +114,7 @@ To cancel and reschedule atomically, wrap both calls in `announce`: bytes[] memory calls = new bytes[](2); calls[0] = abi.encodeCall(IB20Asset.cancelUIMultiplierUpdate, ()); calls[1] = abi.encodeCall(IB20Asset.updateUIMultiplier, (secondMultiplier, secondEffectiveAt)); -asset.announce(calls, "2026-split-02", "Rescheduled 2-for-1 stock split", "https://example.com/corporate-actions/2026-split-02"); +asset.announce(calls, "2026-multiplier-02", "Rescheduled multiplier update", "https://example.com/announcements/2026-multiplier-02"); ``` ## Emergency Override diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/announce.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/announce.mdx index c4d1926f7..acb0f40dd 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/announce.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/announce.mdx @@ -1,6 +1,6 @@ --- title: "IB20Asset.announce" -description: "Wraps one or more asset calls in a disclosed corporate-action bracket, emitting Announcement and EndAnnouncement with the same id." +description: "Wraps one or more asset calls in a disclosed announcement bracket, emitting Announcement and EndAnnouncement with the same id." --- ## Signature diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/batch-mint.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/batch-mint.mdx index 5e8c99658..c6bdcc63e 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/batch-mint.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/batch-mint.mdx @@ -59,7 +59,7 @@ amounts[1] = 200e18; IB20Asset(asset).batchMint(recipients, amounts); ``` -Use `batchMint` inside an `announce` call to disclose a dividend issuance or additional mint as a corporate action: +Use `batchMint` inside an `announce` call to disclose a dividend issuance or additional mint in an announcement: ```solidity Announced Batch Mint bytes[] memory calls = new bytes[](1); diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/operator-role.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/operator-role.mdx index 309069f2f..a1bd2165c 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/operator-role.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/operator-role.mdx @@ -1,6 +1,6 @@ --- title: "IB20Asset.OPERATOR_ROLE" -description: "Returns the OPERATOR_ROLE constant that gates multiplier scheduling and corporate action announcement on B20 Asset." +description: "Returns the OPERATOR_ROLE constant that gates multiplier scheduling and announcements on B20 Asset." --- ## Signature @@ -18,7 +18,7 @@ function OPERATOR_ROLE() external view returns (bytes32); Returns the `bytes32` role constant required to call: -- `announce`: disclose a corporate action +- `announce`: disclose an operator-driven change - `updateUIMultiplier`: schedule a future multiplier change - `cancelUIMultiplierUpdate`: cancel a live pending multiplier update - `updateMultiplier`: immediate override (deprecated; emergency use only) diff --git a/docs/specifications/b20/reference/interfaces/ib20-asset/update-multiplier.mdx b/docs/specifications/b20/reference/interfaces/ib20-asset/update-multiplier.mdx index 60a528cb2..604802b23 100644 --- a/docs/specifications/b20/reference/interfaces/ib20-asset/update-multiplier.mdx +++ b/docs/specifications/b20/reference/interfaces/ib20-asset/update-multiplier.mdx @@ -4,7 +4,7 @@ description: "Deprecated instant multiplier override that sets the multiplier im --- -`updateMultiplier` is deprecated. Use `updateUIMultiplier` for routine stock splits and corporate actions. Reserve this function for emergency overrides only. +`updateMultiplier` is deprecated. Use `updateUIMultiplier` for routine multiplier updates. Reserve this function for emergency overrides only. ## Signature diff --git a/examples/verified-doc-samples/solidity/script/B20Examples.s.sol b/examples/verified-doc-samples/solidity/script/B20Examples.s.sol index 18b138814..6b2acd4c3 100644 --- a/examples/verified-doc-samples/solidity/script/B20Examples.s.sol +++ b/examples/verified-doc-samples/solidity/script/B20Examples.s.sol @@ -135,7 +135,7 @@ contract B20Examples is Script { calls, "2026-stock-dividend-01", "Five-percent stock dividend", - "https://example.com/corporate-actions/2026-01" + "https://example.com/announcements/2026-01" ); require(IB20Asset(token).isAnnouncementIdUsed("2026-stock-dividend-01"), "announcement missing"); }