-
Notifications
You must be signed in to change notification settings - Fork 728
Docs/b20 reference #1775
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
soheimam
wants to merge
1
commit into
master
Choose a base branch
from
docs/b20-reference
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Docs/b20 reference #1775
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
220 changes: 220 additions & 0 deletions
220
docs/base-chain/specs/upgrades/beryl/b20/specification/index.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| --- | ||
| title: "B20 specification" | ||
| description: "Normative Beryl specification for Base's B20 native token standard, precompile registries, policy scopes, factory, and variants." | ||
| --- | ||
|
|
||
| <Note> | ||
| This is the normative Beryl specification for B20. | ||
| </Note> | ||
|
|
||
| B20 is Base's native ERC-20-compatible token standard implemented as Rust precompiles. It adds chain-native roles, policy scopes, memos, pausing, supply caps, ERC-2612 `permit`, deterministic factory creation, and variant-specific surfaces for Asset and Stablecoin tokens. | ||
|
|
||
| ## ERC-20 Compatibility | ||
|
|
||
| B20 is a superset of ERC-20. Standard ERC-20 calls and events keep selector and behavior parity for `transfer`, `transferFrom`, `approve`, `allowance`, `balanceOf`, `totalSupply`, `name`, `symbol`, `decimals`, `Transfer`, and `Approval`. | ||
|
|
||
| B20-specific methods extend the standard without changing the ERC-20 surface. | ||
|
|
||
| ## Roles Model | ||
|
|
||
| B20 includes role-based access control with fixed built-in roles. | ||
|
|
||
| | Role | Gates | | ||
| |---|---| | ||
| | `DEFAULT_ADMIN_ROLE` | `grantRole`, `revokeRole`, `setRoleAdmin`, `updatePolicy`, `updateSupplyCap` | | ||
| | `MINT_ROLE` | `mint`, `mintWithMemo` | | ||
| | `BURN_ROLE` | `burn`, `burnWithMemo` | | ||
| | `BURN_BLOCKED_ROLE` | Deprecated back-compat `burnBlocked` path | | ||
| | `SEIZE_ROLE` | `seizeWithMemo` | | ||
| | `PAUSE_ROLE` | `pause` | | ||
| | `UNPAUSE_ROLE` | `unpause` | | ||
| | `METADATA_ROLE` | `updateName`, `updateSymbol`, `updateContractURI` | | ||
| | `OPERATOR_ROLE` | Asset-only multiplier and announcement operations | | ||
|
|
||
| User-defined roles are supported by the role graph but have no built-in enforcement by B20 token functions. | ||
|
|
||
| ### Admin Renunciation | ||
|
|
||
| The final `DEFAULT_ADMIN_ROLE` holder cannot be removed with normal `renounceRole` or `revokeRole`; both revert with `LastAdminCannotRenounce`. `renounceLastAdmin()` is the only normal path to permanently transition to admin-less operation. | ||
|
|
||
| A token can launch admin-less by setting `initialAdmin == address(0)` at creation. After admin renunciation, `DEFAULT_ADMIN_ROLE`-gated functions are permanently uncallable and admin resurrection is blocked. | ||
|
|
||
| ## Policy Registry | ||
|
|
||
| The PolicyRegistry is a singleton precompile that stores policies addressed by `uint64 policyId`. B20 tokens store policy IDs in fixed scopes and call `isAuthorized(policyId, account)` during gated operations. | ||
|
|
||
| State-changing PolicyRegistry calls are ActivationRegistry-gated. Read functions are always callable. | ||
|
|
||
| ### Policy Types | ||
|
|
||
| | PolicyType | Behavior | | ||
| |---|---| | ||
| | `BLOCKLIST` | Account is authorized unless listed. | | ||
| | `ALLOWLIST` | Account is authorized only if listed. | | ||
| | `UNION` | Composite: account is authorized if any child simple policy authorizes it. | | ||
| | `INTERSECT` | Composite: account is authorized only if every child simple policy authorizes it. | | ||
|
|
||
| Composite policies reference existing simple `ALLOWLIST` or `BLOCKLIST` child policies. They cannot reference composites or built-ins as children. | ||
|
|
||
| ### Policy IDs | ||
|
|
||
| Policy IDs are laid out as: | ||
|
|
||
| ```text | ||
| [top 8 bits: PolicyType][low 56 bits: counter] | ||
| ``` | ||
|
|
||
| Counters `0` and `1` are reserved for built-ins: | ||
|
|
||
| | Built-in | Value | Behavior | | ||
| |---|---:|---| | ||
| | `ALWAYS_ALLOW` | `0` | Authorizes every account. | | ||
| | `ALWAYS_BLOCK` | `(uint64(ALLOWLIST) << 56) \| 1` | Denies every account. | | ||
|
|
||
| Custom policy creation starts at counter `2`. | ||
|
|
||
| ### Admin Model | ||
|
|
||
| Each policy has one admin. Admin transfer is two-step: `stageUpdateAdmin(policyId, newAdmin)` followed by `finalizeUpdateAdmin(policyId)` from the pending admin. `renounceAdmin(policyId)` permanently freezes membership or child-policy updates for that policy. | ||
|
|
||
| ### Read Interface | ||
|
|
||
| | Method | Description | | ||
| |---|---| | ||
| | `isAuthorized(policyId, account)` | Returns authorization and never reverts for uncreated IDs. | | ||
| | `policyExists(policyId)` | Returns whether a policy exists. | | ||
| | `policyAdmin(policyId)` | Returns the current admin or zero. | | ||
| | `pendingPolicyAdmin(policyId)` | Returns the staged admin or zero. | | ||
| | `compositePolicyChildIds(policyId)` | Returns child policy IDs for composite policies. | | ||
|
|
||
| `isAuthorized` collapses uncreated IDs to empty-set semantics. Callers that write policy IDs into token scopes must validate `policyExists` unless writing a built-in. | ||
|
|
||
| ## Policy Integration | ||
|
|
||
| B20 tokens store one `uint64 policyId` per supported policy scope. | ||
|
|
||
| | Scope | Checked account | Operation | | ||
| |---|---|---| | ||
| | `TRANSFER_SENDER_POLICY` | `from` | `transfer`, `transferFrom`, and memo variants | | ||
| | `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 | | ||
|
|
||
| All scopes default to `ALWAYS_ALLOW` at creation. `approve` and `permit` are not policy-gated. | ||
|
|
||
| ## Mint | ||
|
|
||
| `mint` and `mintWithMemo` are gated by `MINT_ROLE`, checked against `MINT_RECEIVER_POLICY`, and bounded by `supplyCap`. | ||
|
|
||
| ## Burn | ||
|
|
||
| `burn` and `burnWithMemo` burn from the caller and are gated by `BURN_ROLE`. | ||
|
|
||
| The legacy `burnBlocked` path is deprecated and retained for backwards compatibility. New seizure flows use `seizeWithMemo`. | ||
|
|
||
| ## 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, and requires `from` to be denied by `SEIZE_HOLDER_POLICY`. | ||
|
|
||
| ## Supply Cap | ||
|
|
||
| The supply cap is optional. The sentinel `type(uint128).max` indicates no practical cap and is also the maximum permitted `totalSupply`. `updateSupplyCap` is admin-gated and reverts with `InvalidSupplyCap` if the proposed cap is below current supply or above the maximum. | ||
|
|
||
| ## Memos | ||
|
|
||
| Memo-enabled operations emit `Memo(address indexed caller, bytes32 indexed memo)` immediately after the primary operation event. Indexers join memo logs to the parent log with `(transactionHash, logIndex - 1)`. | ||
|
|
||
| Memo entrypoints include `transferWithMemo`, `transferFromWithMemo`, `mintWithMemo`, `burnWithMemo`, and `seizeWithMemo`. | ||
|
|
||
| ## Pause | ||
|
|
||
| B20 supports granular pausing by `PausableFeature`: `TRANSFER`, `MINT`, `BURN`, and `SEIZE`. The enum is append-only. `pause` is gated by `PAUSE_ROLE`; `unpause` is gated by `UNPAUSE_ROLE`. | ||
|
|
||
| ## ERC-2612 Permit / EIP-712 | ||
|
|
||
| B20 implements ERC-2612 signed approvals with an EIP-712 domain shaped as `(name, version, chainId, verifyingContract)`, with `version` fixed at `"1"`. `updateName` rotates the domain separator and emits `EIP712DomainChanged`. ERC-1271 contract signatures are not accepted. | ||
|
|
||
| ## Contract URI (ERC-7572) | ||
|
|
||
| `contractURI()` returns offchain token metadata per ERC-7572. `updateContractURI(newURI)` is gated by `METADATA_ROLE`. | ||
|
|
||
| ## Metadata Updates | ||
|
|
||
| `updateName` and `updateSymbol` are gated by `METADATA_ROLE`. `updateName` also rotates the EIP-712 domain separator. | ||
|
|
||
| ## Factory | ||
|
|
||
| All B20 tokens are created through the singleton factory precompile: | ||
|
|
||
| ```solidity | ||
| createB20(B20Variant variant, bytes32 salt, bytes params, bytes[] initCalls) | ||
| ``` | ||
|
|
||
| | Parameter | Description | | ||
| |---|---| | ||
| | `variant` | `ASSET` or `STABLECOIN` | | ||
| | `salt` | Caller-chosen entropy for deterministic address derivation | | ||
| | `params` | Versioned, variant-specific create params | | ||
| | `initCalls` | ABI-encoded bootstrap calls dispatched to the new token | | ||
|
|
||
| The factory reverts with `FeatureNotActivated` if the requested variant is not activated. | ||
|
|
||
| ### Address Derivation | ||
|
|
||
| B20 token addresses are deterministic and encode the variant: | ||
|
|
||
| ```text | ||
| [10-byte B20 prefix][1-byte variant][9-byte keccak256(deployer, salt)] | ||
| ``` | ||
|
|
||
| `getB20Address`, `isB20`, and `isB20Initialized` are available on the factory. | ||
|
|
||
| ### initCalls Semantics | ||
|
|
||
| During initCalls, factory-originated calls bypass token role gates and transfer-side policy gates: `TRANSFER_SENDER_POLICY`, `TRANSFER_RECEIVER_POLICY`, and `TRANSFER_EXECUTOR_POLICY`. | ||
|
|
||
| The bypass does not apply to `MINT_RECEIVER_POLICY`, pause state, supply cap, or balance accounting invariants. The bootstrap window closes when `createB20` returns. | ||
|
|
||
| ## Variants | ||
|
|
||
| | Variant | Byte | Decimals | Additional surface | | ||
| |---|---:|---|---| | ||
| | `ASSET` | `0x00` | 6-18, configured at creation | Multiplier, announcements, batch mint, extra metadata | | ||
| | `STABLECOIN` | `0x01` | Fixed 6 | `currency()` | | ||
|
|
||
| ### Asset | ||
|
|
||
| Asset tokens add `OPERATOR_ROLE`, scaled UI balance support, scheduled and instant multiplier updates, announcements, batch minting, and extra metadata. | ||
|
|
||
| #### Multiplier | ||
|
|
||
| The multiplier is WAD-precision and scales UI balance reads while raw balances remain unchanged. | ||
|
|
||
| #### Announcements | ||
|
|
||
| `announce` emits `Announcement`, dispatches internal calls, and emits `EndAnnouncement`. Announcement IDs are unique forever. Non-panic inner reverts are wrapped in `InternalCallFailed`. | ||
|
|
||
| #### Batch Mint | ||
|
|
||
| `batchMint` mints to parallel recipient and amount arrays atomically and is gated by `MINT_ROLE`. | ||
|
|
||
| #### Extra Metadata | ||
|
|
||
| `extraMetadata(key)` reads issuer-defined metadata. `updateExtraMetadata(key, value)` writes it and deletes the entry when `value` is empty. | ||
|
|
||
| ### Stablecoin | ||
|
|
||
| Stablecoin tokens add `currency()`, set once at creation. The value must contain uppercase `A`-`Z` characters only. B20 validates the code format, not the issuer claim, reserves, legal status, or external registration. | ||
|
|
||
| ## Precompile addresses | ||
|
|
||
| | Precompile | Address | | ||
| |---|---| | ||
| | B20Factory | `0xB20f000000000000000000000000000000000000` | | ||
| | ActivationRegistry | `0x8453000000000000000000000000000000000001` | | ||
| | PolicyRegistry | `0x8453000000000000000000000000000000000002` | | ||
|
|
||
| ## Developer documentation | ||
|
|
||
| - [Generated reference](/base-chain/specs/upgrades/beryl/b20/specification/reference/interfaces/IB20) | ||
85 changes: 85 additions & 0 deletions
85
...in/specs/upgrades/beryl/b20/specification/reference/constants-and-addresses.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| --- | ||
| title: "Constants & addresses" | ||
| description: "Copy B20 precompile addresses, roles, policy scopes, variant bytes, policy IDs, and supply-cap constants." | ||
| --- | ||
|
|
||
| Use `StdPrecompiles.sol` and `B20Constants.sol` from `base-std` as the canonical Solidity source. This page lists the same values for quick reference. | ||
|
|
||
| ## Precompile addresses | ||
|
|
||
| These addresses are identical on every network where B20 is active. | ||
|
|
||
| | Surface | Address | | ||
| |---|---| | ||
| | B20 Factory | `0xB20f000000000000000000000000000000000000` | | ||
| | ActivationRegistry | `0x8453000000000000000000000000000000000001` | | ||
| | PolicyRegistry | `0x8453000000000000000000000000000000000002` | | ||
|
|
||
| ## Roles | ||
|
|
||
| | Role | Solidity constant | | ||
| |---|---| | ||
| | Default admin | `bytes32(0)` | | ||
| | Mint | `keccak256("MINT_ROLE")` | | ||
| | Burn | `keccak256("BURN_ROLE")` | | ||
| | Deprecated blocked burn | `keccak256("BURN_BLOCKED_ROLE")` | | ||
| | Seize | `keccak256("SEIZE_ROLE")` | | ||
| | Pause | `keccak256("PAUSE_ROLE")` | | ||
| | Unpause | `keccak256("UNPAUSE_ROLE")` | | ||
| | Metadata | `keccak256("METADATA_ROLE")` | | ||
| | Asset operator | `keccak256("OPERATOR_ROLE")` | | ||
|
|
||
| ## Policy scopes | ||
|
|
||
| | Scope | Solidity constant | | ||
| |---|---| | ||
| | Transfer sender | `keccak256("TRANSFER_SENDER_POLICY")` | | ||
| | Transfer receiver | `keccak256("TRANSFER_RECEIVER_POLICY")` | | ||
| | Transfer executor | `keccak256("TRANSFER_EXECUTOR_POLICY")` | | ||
| | Mint receiver | `keccak256("MINT_RECEIVER_POLICY")` | | ||
| | Seize holder | `keccak256("SEIZE_HOLDER_POLICY")` | | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing the 6th policy scope: |
||
|
|
||
| ## Policy IDs | ||
|
|
||
| | Name | Value | | ||
| |---|---| | ||
| | `ALWAYS_ALLOW` | `0` | | ||
| | `ALWAYS_BLOCK` | `(uint64(uint8(IPolicyRegistry.PolicyType.ALLOWLIST)) << 56) \| 1` | | ||
|
|
||
| Custom policy IDs use this layout: | ||
|
|
||
| ```text | ||
| [8-bit PolicyType][56-bit counter] | ||
| ``` | ||
|
|
||
| Policy type bytes: | ||
|
|
||
| | PolicyType | Byte | | ||
| |---|---:| | ||
| | `BLOCKLIST` | `0x00` | | ||
| | `ALLOWLIST` | `0x01` | | ||
| | `UNION` | `0x02` | | ||
| | `INTERSECT` | `0x03` | | ||
|
|
||
| ## Variant bytes | ||
|
|
||
| | Variant | Byte | Address shape | | ||
| |---|---:|---| | ||
| | `ASSET` | `0x00` | `0xB200...` | | ||
| | `STABLECOIN` | `0x01` | `0xB201...` | | ||
|
|
||
| ## Supply and decimals | ||
|
|
||
| | Constant | Value | | ||
| |---|---:| | ||
| | Minimum Asset decimals | `6` | | ||
| | Maximum Asset decimals | `18` | | ||
| | Maximum supply cap / no-cap sentinel | `type(uint128).max` | | ||
| | All features paused bitmask | `15` (`TRANSFER | MINT | BURN | SEIZE`) | | ||
|
|
||
| ## Imports | ||
|
|
||
| ```solidity | ||
| import {StdPrecompiles} from "base-std/StdPrecompiles.sol"; | ||
| import {B20Constants} from "base-std/lib/B20Constants.sol"; | ||
| ``` | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Policy Integration table is missing
SEIZE_RECEIVER_POLICY(6th scope), and the Seize section (~line 118) saysseizeWithMemoonly requiresfromto be denied bySEIZE_HOLDER_POLICY, it omits thattois gated bySEIZE_RECEIVER_POLICY(IB20.sol:460–467).