This is the normative Beryl specification for B20. For developer-oriented concepts, implementation guides, and generated interface reference, start with the B20 token standard docs.
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 fortransfer, 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.
User-defined roles are supported by the role graph but have no built-in enforcement by B20 token functions.
Admin Renunciation
The finalDEFAULT_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 byuint64 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
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:0 and 1 are reserved for built-ins:
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
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 oneuint64 policyId per supported policy scope.
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 sentineltype(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 emitMemo(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 byPausableFeature: 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:
The factory reverts with
FeatureNotActivated if the requested variant is not activated.
Address Derivation
B20 token addresses are deterministic and encode the variant: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
Asset
Asset tokens addOPERATOR_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 addcurrency(), 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.