-
Notifications
You must be signed in to change notification settings - Fork 727
docs: add B20 concept docs #1773
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-concepts
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
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
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
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
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
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
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
53 changes: 53 additions & 0 deletions
53
...pecs/upgrades/beryl/b20/specification/concepts/architecture-and-precompiles.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,53 @@ | ||
| --- | ||
| title: "Architecture & precompiles" | ||
| description: "Understand the B20 precompile architecture, fixed addresses, activation gates, and deterministic token addresses." | ||
| --- | ||
|
|
||
| A B20 token is not a contract you deploy. It is a chain-native token surface implemented as a Rust precompile and created through the singleton B20 Factory. Solidity and offchain clients still call normal ABI methods, but there is no token bytecode to verify on an explorer. | ||
|
|
||
| ## System map | ||
|
|
||
| <Frame> | ||
|  | ||
| </Frame> | ||
|
|
||
| | Surface | Address | | ||
| |---|---| | ||
| | B20 Factory | `0xB20f000000000000000000000000000000000000` | | ||
| | ActivationRegistry | `0x8453000000000000000000000000000000000001` | | ||
| | PolicyRegistry | `0x8453000000000000000000000000000000000002` | | ||
|
|
||
| These addresses are identical on every network where B20 is active. | ||
|
|
||
| ## Activation gates | ||
|
|
||
| The ActivationRegistry is the chain-level feature flag surface. The factory checks activation before creating a token variant, and state-changing PolicyRegistry calls are activation-gated. | ||
|
|
||
| <Check> | ||
| PolicyRegistry read functions are always callable. Integrations can safely call `isAuthorized`, `policyExists`, `policyAdmin`, and `pendingPolicyAdmin` without an activation write path. | ||
| </Check> | ||
|
|
||
| ## Address derivation | ||
|
|
||
| B20 token addresses are deterministic and include the variant byte: | ||
|
|
||
| ```text | ||
| [10-byte B20 prefix][1-byte variant][9-byte keccak256(deployer, salt)] | ||
| ``` | ||
|
|
||
| You can identify a token's variant from the address without an RPC call by inspecting byte 10, zero-indexed: | ||
|
|
||
| | Variant | Byte | Address shape | | ||
| |---|---:|---| | ||
| | Asset | `0x00` | `0xB200...` | | ||
| | Stablecoin | `0x01` | `0xB201...` | | ||
|
|
||
| Use `IB20Factory.getB20Address(variant, deployer, salt)` to precompute the address and `isB20` / `isB20Initialized` to verify it. | ||
|
|
||
| ## What precompiles change for developers | ||
|
|
||
| - Import interfaces from `base-std` instead of copying ABIs by hand. | ||
| - Use `StdPrecompiles` handles for the factory and registries. | ||
| - Do not expect explorer bytecode verification for token or registry addresses. | ||
| - Test with base-std mocks, `base-forge`, or `base-anvil` because stock EVM tooling does not know these precompiles by default. | ||
|
|
90 changes: 90 additions & 0 deletions
90
...e-chain/specs/upgrades/beryl/b20/specification/concepts/policies-and-scopes.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,90 @@ | ||
| --- | ||
| title: "Policies & scopes" | ||
| description: "Learn how B20 policy scopes point to PolicyRegistry policies and gate transfers, mints, and seizures." | ||
| --- | ||
|
|
||
| A B20 token chooses policies by scope. Each scope stores one `uint64` policy ID that points to a policy in the singleton PolicyRegistry. The token only checks the policy assigned to the relevant scope. | ||
|
|
||
| <Frame> | ||
|  | ||
| </Frame> | ||
|
|
||
| On a gated operation, the token reads the relevant scope, calls `PolicyRegistry.isAuthorized(policyId, account)`, and reverts with `PolicyForbids` or a seize-specific error when the policy result does not permit the action. | ||
|
|
||
| ## Policy scopes | ||
|
|
||
| | Scope | Checked account | Gated 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` only when `msg.sender != from` | | ||
| | `MINT_RECEIVER_POLICY` | `to` | `mint` and `mintWithMemo` | | ||
| | `SEIZE_HOLDER_POLICY` | `from` | `seizeWithMemo`; the holder is seizable only when not authorized by this policy | | ||
|
|
||
| `approve` and `permit` are not policy-gated. Only balance movement is checked. | ||
|
|
||
| <Warning> | ||
| Every scope defaults to `ALWAYS_ALLOW` (`0`) at creation. An unattended B20 deployment is fully open, and no account is seizable until `SEIZE_HOLDER_POLICY` is intentionally configured. | ||
| </Warning> | ||
|
|
||
| ## Policy types | ||
|
|
||
| | Type | Behavior | | ||
| |---|---| | ||
| | `BLOCKLIST` | Accounts are authorized by default; listed accounts are denied. | | ||
| | `ALLOWLIST` | Accounts are denied by default; listed accounts are authorized. | | ||
| | `UNION` | Composite policy: account is authorized if any child policy authorizes it. | | ||
| | `INTERSECT` | Composite policy: account is authorized only if every child policy authorizes it. | | ||
|
|
||
| Composite policies combine existing simple `ALLOWLIST` and `BLOCKLIST` policies. Child policies must be simple policies, not other composites or built-ins. | ||
|
|
||
| ## Built-ins and ID anatomy | ||
|
|
||
| Policy IDs are `uint64` values. The top byte is the `PolicyType`; the low 56 bits are a global counter. Counters `0` and `1` are reserved for built-ins, and custom policy creation starts at counter `2`. | ||
|
|
||
| | Constant | ID | Behavior | | ||
| |---|---:|---| | ||
| | `ALWAYS_ALLOW` | `0` | Authorizes every account. Default value for every scope. | | ||
| | `ALWAYS_BLOCK` | `(uint64(ALLOWLIST) << 56) \| 1` | Denies every account. | | ||
|
|
||
| <Warning> | ||
| `isAuthorized` never reverts on a non-existent policy ID. Malformed or uncreated IDs collapse to empty-set semantics: `ALLOWLIST` denies and `BLOCKLIST` allows. Validate `policyExists(policyId)` before binding a token scope. | ||
| </Warning> | ||
|
|
||
| ```solidity | ||
| uint64 policyId = 0x0100000000000002; | ||
| require(StdPrecompiles.POLICY_REGISTRY.policyExists(policyId), "policy missing"); | ||
| IB20(token).updatePolicy(B20Constants.MINT_RECEIVER_POLICY, policyId); | ||
| ``` | ||
|
|
||
| ## Registry administration | ||
|
|
||
| Each policy has one admin. The current admin can: | ||
|
|
||
| - Update allowlist, blocklist, or composite membership. | ||
| - Stage a two-step admin transfer with `stageUpdateAdmin`. | ||
| - Permanently renounce policy administration with `renounceAdmin`. | ||
|
|
||
| <Warning> | ||
| `renounceAdmin(policyId)` freezes the policy forever. Membership and child-policy updates become impossible. | ||
| </Warning> | ||
|
|
||
| ## Read a token's configured policies | ||
|
|
||
| ```solidity | ||
| bytes32[5] memory scopes = [ | ||
| B20Constants.TRANSFER_SENDER_POLICY, | ||
| B20Constants.TRANSFER_RECEIVER_POLICY, | ||
| B20Constants.TRANSFER_EXECUTOR_POLICY, | ||
| B20Constants.MINT_RECEIVER_POLICY, | ||
| B20Constants.SEIZE_HOLDER_POLICY | ||
| ]; | ||
|
|
||
| for (uint256 i; i < scopes.length; i++) { | ||
| uint64 id = IB20(token).policyId(scopes[i]); | ||
| bool exists = id == 0 || id == ((uint64(uint8(IPolicyRegistry.PolicyType.ALLOWLIST)) << 56) | 1) | ||
| || StdPrecompiles.POLICY_REGISTRY.policyExists(id); | ||
| require(exists, "scope points to missing policy"); | ||
| } | ||
| ``` | ||
|
|
||
63 changes: 63 additions & 0 deletions
63
...in/specs/upgrades/beryl/b20/specification/concepts/roles-and-access-control.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,63 @@ | ||
| --- | ||
| title: "Roles & access control" | ||
| description: "Understand B20 roles, access-control gates, user-defined roles, and irreversible admin renunciation." | ||
| --- | ||
|
|
||
| B20 is an ERC-20 superset with built-in role-based access control. Its role API uses familiar admin, grant, revoke, and renounce patterns, but B20 only enforces the built-in roles listed here. | ||
|
|
||
| ## Base roles | ||
|
|
||
| | Role | Gates | | ||
| |---|---| | ||
| | `DEFAULT_ADMIN_ROLE` | Role grants/revokes, `setRoleAdmin`, `updatePolicy`, `updateSupplyCap` | | ||
| | `MINT_ROLE` | `mint`, `mintWithMemo` | | ||
| | `BURN_ROLE` | `burn`, `burnWithMemo` | | ||
| | `BURN_BLOCKED_ROLE` | Deprecated back-compat `burnBlocked` implementation only | | ||
| | `SEIZE_ROLE` | `seizeWithMemo` | | ||
| | `PAUSE_ROLE` | `pause` | | ||
| | `UNPAUSE_ROLE` | `unpause` | | ||
| | `METADATA_ROLE` | `updateName`, `updateSymbol`, `updateContractURI` | | ||
| | `OPERATOR_ROLE` | Asset-only multiplier and announcement operations | | ||
|
|
||
| <Warning> | ||
| User-defined roles have no built-in enforcement. You can create them with `setRoleAdmin` and grant them with `grantRole`, but B20 token functions only check the built-in roles. | ||
| </Warning> | ||
|
|
||
| ## Admin renunciation | ||
|
|
||
| B20 has a specific last-admin rule. The last `DEFAULT_ADMIN_ROLE` holder cannot renounce or be revoked through normal role methods; those calls revert with `LastAdminCannotRenounce`. | ||
|
|
||
| Use `renounceLastAdmin()` to permanently move the token to an admin-less state. A token can also launch admin-less by passing `initialAdmin == address(0)` at creation. | ||
|
|
||
| After admin renunciation: | ||
|
|
||
| - `DEFAULT_ADMIN_ROLE`-gated operations are permanently uncallable. | ||
| - Existing operational role grants continue to work. | ||
| - Admin resurrection is blocked; `grantRole`, `revokeRole`, and `setRoleAdmin` revert even through custom admin-role chains. | ||
|
|
||
| ## Pre-renunciation checklist | ||
|
|
||
| Configure every surviving operational path before renouncing the last admin. | ||
|
|
||
| ```solidity | ||
| IB20 token = IB20(tokenAddress); | ||
|
|
||
| // 1. Grant roles that must survive admin renunciation. | ||
| token.grantRole(B20Constants.MINT_ROLE, issuerOps); | ||
| token.grantRole(B20Constants.PAUSE_ROLE, incidentResponder); | ||
| token.grantRole(B20Constants.UNPAUSE_ROLE, governanceSafe); | ||
| token.grantRole(B20Constants.SEIZE_ROLE, complianceSafe); | ||
|
|
||
| // 2. Bind policies and supply cap while DEFAULT_ADMIN_ROLE still exists. | ||
| token.updatePolicy(B20Constants.MINT_RECEIVER_POLICY, mintAllowlistPolicyId); | ||
| token.updatePolicy(B20Constants.SEIZE_HOLDER_POLICY, seizeHolderPolicyId); | ||
| token.updateSupplyCap(1_000_000e18); | ||
|
|
||
| // 3. Permanently remove token administration. | ||
| token.renounceLastAdmin(); | ||
| ``` | ||
|
|
||
| <Note> | ||
| For an admin-less launch from creation, put the required grants and policy updates in `initCalls`, then set `initialAdmin` to `address(0)` in the create params. | ||
| </Note> | ||
|
|
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.
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.
Missing the 6th policy scope.
B20Constants.sol/IB20.sol:274defineSEIZE_RECEIVER_POLICY(gates the seizetorecipient), so there are 6 scopes, not 5. Add a row here and update thebytes32[5]array below (~line 75) to include it — as written the audit sample silently skips a real scope.