1. Abstract
B20 is the Base ecosystem’s native token standard — an ERC-20 superset implemented as Rust precompiles rather than EVM smart contracts. It ships with a built-in compliance toolkit: transfer policies, freeze-and-seize, role-based access control, memos, and supply caps. All tokens are deployed via the singleton B20Factory precompile. The full interface definitions are available in the Base Standard Library repository. To deploy your first token, see the Launch a B20 token quickstart.2. ERC-20 Compatibility
B20 is a strict superset of ERC-20. Every ERC-20 call (transfer, transferFrom, approve, balanceOf, allowance, and the standard events) behaves exactly as the standard specifies, so existing ERC-20 tooling and integrations work against B20 with no changes.
B20 adds methods that ERC-20 does not include: memos, mint/burn, policy gating, granular pause, and ERC-2612 permit. These extend ERC-20 without altering it — every ERC-20 method exists on B20, but the reverse does not hold.
Because B20 runs as a native precompile, tokens are cheaper and higher-throughput than contract-based tokens while maintaining full ERC-20 wire compatibility.
3. Variants
B20 supports two token variants. Each shares the full base surface (ERC-20, roles, policies, pause, memos, permit) and adds capabilities specific to its use case.
The variant byte is encoded directly in the token address at byte 10, making it identifiable without state queries.
A single token standard cannot serve all use cases equally. Equities need stock splits (multiplier), SEC-style disclosure brackets (announcements), and CUSIP/ISIN storage (extra metadata). Stablecoins need a fixed decimal convention matching fiat (6 decimals) and an on-chain currency code for automated classification. Two concrete variants let each type carry only what it needs.
See Asset variant and Stablecoin variant for each variant’s unique surface.
4. Policy Registry
The Policy Registry is a singleton precompile that manages allowlists and blocklists. B20 tokens reference policies byuint64 ID — they do not store membership data themselves. Any caller can create a policy and nominate its admin, decoupling policy management from token issuance.
Centralizing list management in a shared precompile means one policy can gate multiple tokens, and one compliance team can manage one blocklist that applies across their entire portfolio.
State-changing functions on the Policy Registry are gated by the Activation Registry, which tracks which Base features are live. Read functions (
isAuthorized, policyExists, policyAdmin, pendingPolicyAdmin) are always callable.4.1. Policy Types
4.2. Policy IDs
Policy IDs areuint64 values. The top byte encodes the PolicyType; the low 56 bits are a global counter starting at 2.
4.3. isAuthorized Semantics
isAuthorized(policyId, account) never reverts, even on a non-existent policy ID. Non-existent policies collapse to empty-member-set semantics:
4.4. Creating and Managing Policies
bool parameter sets the membership state.
4.5. Admin Model
Each policy has exactly one admin. Admin transfers use a two-step pattern to prevent accidental transfers to wrong addresses:- Current admin calls
stageUpdateAdmin(policyId, newAdmin) - Pending admin calls
finalizeUpdateAdmin(policyId)
renounceAdmin(policyId) permanently freezes the policy — membership can never be changed again. This is irreversible. There is no grantAdmin or path to restore an admin after renouncement.
4.6. Read Interface
5. Roles and Access Control
B20 uses a fixed set of roles to gate privileged operations. The model extends OpenZeppelinAccessControl with one behavioral override: the last admin cannot be removed through standard revocation.
Token issuers need fine-grained control over who can mint, burn, pause, and configure a token. A single “owner” pattern is too coarse — an issuer may want their treasury to mint while a separate compliance team manages pauses, without either party having the other’s privileges. The fixed role set ensures every B20 token has the same permission surface, making tooling, audits, and integrations predictable across the ecosystem.
5.1. Base Roles
The first seven roles exist on all B20 tokens.
OPERATOR_ROLE is exclusive to the Asset variant.
5.2. Custom Roles
User-defined roles are supported viasetRoleAdmin and grantRole. They carry no built-in effect — B20 only enforces gates against the eight base-surface roles listed above. Custom roles can be used by external contracts that call hasRole to implement additional access patterns.
5.3. Admin Renunciation
The lastDEFAULT_ADMIN_ROLE holder cannot be removed via renounceRole or revokeRole (both revert with LastAdminCannotRenounce). The dedicated renounceLastAdmin() is the only path to permanently transition a token to admin-less operation.
initialAdmin == address(0) at creation, which never grants the role and skips the renounceLastAdmin step entirely.
After renounceLastAdmin() (or for tokens deployed with initialAdmin == address(0)):
- Operations gated by
DEFAULT_ADMIN_ROLEbecome permanently uncallable - Roles already granted to other addresses (
MINT_ROLE,BURN_ROLE, etc.) continue to function independently - Admin resurrection is blocked:
grantRole,revokeRole, andsetRoleAdminall revert withAccessControlUnauthorizedAccount - The token’s policies, supply cap, and metadata become immutable
AccessControl allows the last admin to renounceRole themselves, which is a footgun. B20 blocks this path and requires renounceLastAdmin() — a function that does nothing else and whose name makes the consequence unmistakable.
5.4. Pause and Unpause Role Separation
PAUSE_ROLE and UNPAUSE_ROLE are intentionally separate. This enables security architectures where a monitoring bot can emergency-pause on anomaly detection without having the power to unpause. Only governance (a multisig, timelock, or higher-authority key) can restore operations — preventing a compromised pauser from toggling pause to mask an attack.
6. Transfer Policies
B20 declares a fixed set of policy scopes that reference the Policy Registry. Each scope stores auint64 policy ID. On every gated operation, B20 calls isAuthorized against the relevant scope and reverts if the account is not authorized.
A bare ERC-20 transfer moves tokens between any two addresses with no concept of authorized parties. Regulated issuers need to enforce KYC/AML, sanctions compliance, and jurisdiction restrictions at the transfer level. B20 externalizes these rules to the Policy Registry and connects token operations to policies via scopes — compliance rules can be updated without modifying the token, multiple tokens can share the same policy, and different aspects of a transfer can be gated by different policies.
6.1. Scopes
6.2. Evaluation Order
For atransferFrom(from, to, amount) call where msg.sender != from:
- Check
TRANSFER_SENDER_POLICYagainstfrom - Check
TRANSFER_RECEIVER_POLICYagainstto - Check
TRANSFER_EXECUTOR_POLICYagainstmsg.sender
PolicyForbids.
For a transfer(to, amount) call, only TRANSFER_SENDER_POLICY (against msg.sender) and TRANSFER_RECEIVER_POLICY (against to) are checked. TRANSFER_EXECUTOR_POLICY is not checked because the sender and executor are the same account.
6.3. Approve Exemption
approve is not policy-gated. Only actual balance movement via transfer / transferFrom is checked. An account on a blocklist can approve a spender, but the spender’s transferFrom will revert when the sender policy check fails. Gating approve would add no security value since the transfer itself is the enforcement point.
6.4. Default Values
6.5. Reading and Writing Scopes
6.6. initCalls Bypass
During token creation via the Factory,initCalls bypass the three transfer-side policy scopes:
This allows bootstrap transfers (e.g., initial distribution) in the same transaction as deployment, while still ensuring mint recipients pass policy checks from the start.
MINT_RECEIVER_POLICY is never bypassed because mint creates new exposure — a compromised minter that bypasses receiver checks during init could distribute tokens to sanctioned addresses before the compliance program is configured.
7. Mint and Supply Cap
New token supply is created viamint and mintWithMemo, gated by MINT_ROLE. Every mint checks the recipient against MINT_RECEIVER_POLICY. An optional supply cap bounds totalSupply.
Token issuance is the most consequential supply-side operation. Three controls are necessary: role gating prevents unauthorized inflation, receiver policy ensures newly minted tokens only reach verified recipients (mint is the first moment a token enters an account — there is no prior transfer to gate), and the supply cap provides a hard ceiling that no minter can exceed.
7.1. Mint Methods
MINT_ROLE, check to against MINT_RECEIVER_POLICY, check that totalSupply + amount does not exceed the supply cap, and revert if the MINT pausable feature is paused.
7.2. Supply Cap
The supply cap is auint128 upper bound on totalSupply.
updateSupplyCap constraints:
newCapmust be>= totalSupply(cannot set a cap below current supply)newCapmust be<= type(uint128).max(the sentinel is also the maximum)- Emits
SupplyCapUpdated(uint128 oldCap, uint128 newCap)
totalSupply, creating headroom under the cap without requiring admin cap adjustment.
ERC-20 uses uint256 for balances, but uint128 (max ~340 undecillion) exceeds any realistic token supply. Using uint128 for the cap allows the sentinel value to represent “no cap” without consuming the full uint256 range.
8. Factory and Deployment
All B20 tokens are created through the singleton B20Factory precompile. Token addresses are deterministic and encode the variant directly, allowing off-chain identification without RPC calls. The factory supportsinitCalls — a bootstrap window where admin-gated configuration and initial transfers can execute in the same transaction as deployment.
8.1. createB20
The factory is exposed in
base-std as StdPrecompiles.B20_FACTORY. createB20 reverts with IActivationRegistry.FeatureNotActivated if the requested variant’s feature is not yet activated.
Asset params:
Stablecoin params:
8.2. Address Derivation
B20 addresses are deterministic and encode the variant directly:8.3. initCalls Semantics
initCalls are dispatched after token creation in the same transaction. During this bootstrap window, factory-originated calls receive special bypass privileges:
Typical
initCalls sequence:
initCall reverts the entire deployment. Each (deployer, variant, salt) tuple produces exactly one address — deploying the same tuple twice reverts.
9. Burn and Seize
B20 provides two distinct burn paths. Standardburn lets authorized callers destroy tokens from their own balance. burnBlocked lets authorized callers destroy tokens from a third party’s balance — but only if that account is already denied by TRANSFER_SENDER_POLICY.
Unrestricted third-party burns would allow anyone with burn capability to destroy anyone’s tokens. B20 mitigates this by requiring that the target account be frozen first. This ensures seizure is a two-step, auditable process: freeze first, seize second.
9.1. Self-Burn
BURN_ROLE. Burns from msg.sender’s balance. Reverts if the BURN pausable feature is paused.
9.2. Seize (burnBlocked)
BURN_BLOCKED_ROLE. The target account must be denied by TRANSFER_SENDER_POLICY — if the target is not frozen, the call reverts. BURN_BLOCKED_ROLE is separate from BURN_ROLE — combining both in one role would mean every party authorized to redeem could also seize.
9.3. Freeze-and-Seize Workflow
10. Asset Variant
The general-purpose variant for tokenized equities, real-world assets, and long-tail tokens. In addition to the full B20 base surface, Asset tokens add several capabilities gated byOPERATOR_ROLE and existing roles.
10.1. Multiplier
A WAD-precision (1e18) rebase multiplier applied to all balance reads. Raw balances are stored unchanged; the multiplier scales the view returned to callers.balanceOf returns the scaled balance (raw × multiplier). transfer and transferFrom operate on scaled amounts. A multiplier update changes every holder’s visible balance simultaneously — this is a stock-split-equivalent operation.
10.2. Announcements
On-chain disclosure brackets that wrap sensitive operations with a public notice. Announcements create an auditable record that an operation was intentional and pre-disclosed.Announcement(id, description, uri) emitted → internalCalls executed in order → EndAnnouncement(id) emitted.
The id must be unique across the token’s lifetime — reuse reverts with DuplicateAnnouncementId. Permanent uniqueness ensures every announcement is independently verifiable and tamper-evident. Inner call reverts are wrapped in InternalCallFailed.
10.3. Batch Mint
mint calls: MINT_ROLE required, MINT_RECEIVER_POLICY checked for each recipient, supply cap enforced across the total.
10.4. Extra Metadata
An arbitrary key/value store for issuer-defined on-chain metadata (e.g., CUSIP, ISIN, or custom identifiers).11. Stablecoin Variant
The fixed-decimals, fiat-backed variant. Decimals are hard-wired to6 and not configurable. This matches the precision of most fiat currencies and ensures consistency across all B20 stablecoins.
11.1. Currency Code
The currency code is a self-declaration by the issuer. It is not verified against ISO 4217 or any other standard. Integrators should treat it as a label, not a guarantee of peg or backing.
12. Pause
B20 pause is granular: three independently controllable features can be paused and unpaused individually.PAUSE_ROLE and UNPAUSE_ROLE are separate roles.
If a vulnerability is discovered in the burn path, a global pause would halt all transfers — freezing every holder’s liquidity to address a problem in an unrelated operation. Granular pause lets issuers halt only the affected operation.
12.1. Pausable Features
The
PausableFeature enum is append-only — new features may be added in future upgrades but existing values are never changed or removed.
12.2. Feature Interaction Matrix
approve and permit are never affected by pause state. Pause state is never bypassed during factory initCalls. Pausing an already-paused feature is a no-op (does not revert).
13. Memos
A memo is an optionalbytes32 payload attached to a token operation for off-chain reference. On-chain token operations often correspond to off-chain events — invoice payments, redemption references, compliance case IDs. Memos embed the reference directly in the transaction, making the association immutable and auditable.
13.1. Memo Event
Memo event is emitted immediately after the parent operation’s primary event. Both fields are indexed for efficient log filtering.
13.2. Memo-Emitting Entrypoints
Each memo method behaves identically to its non-memo counterpart — same role gates, same policy checks, same pause enforcement.
13.3. Indexer Join Pattern
To associate aMemo event with its parent operation:
Memo event at log index N, the parent Transfer event is at log index N - 1 in the same transaction. This relationship is guaranteed by the B20 precompile’s event emission ordering.
indexer-example.js
bytes32(0) is valid — the Memo event is still emitted. If no memo is needed, use the non-memo method variant instead to save gas.
The bytes32 size is deliberately constrained: large enough for UUIDs, hashes, and encoded identifiers, but small enough to discourage storing data that should live off-chain. Separate *WithMemo methods preserve the standard ERC-20 interface while extending it.
14. Permit (ERC-2612)
B20 implements ERC-2612 signed approvals using an EIP-712 typed-data domain. Token holders can authorize spending via off-chain signatures, enabling gasless approval flows where a relayer submits the permit transaction.14.1. EIP-712 Domain
14.2. permit
allowance(owner, spender) = value and emits Approval(owner, spender, value). Each account has a monotonically increasing nonce — each successful permit call increments the owner’s nonce, preventing signature replay.
14.3. Domain Separator Rotation
WhenupdateName(newName) is called, the EIP-712 domain separator is recomputed with the new name and EIP712DomainChanged (ERC-5267) is emitted. Permits signed with the old name become invalid. Integrations that cache the domain separator must re-fetch it after observing this event.
14.4. Signature Constraints
- ECDSA only —
ecrecoveris used for verification. ERC-1271 contract signatures (smart wallet signatures) are not accepted. Supporting contract signatures would require calling into arbitrary contracts during permit verification, introducing reentrancy and gas-estimation complexity in the precompile environment. - Deadline enforcement —
block.timestamp > deadlinecauses a revert. - Nonce binding — the signed nonce must match the account’s current nonce.
permitis not affected by pause state (it only sets allowance, it does not transfer tokens) and is not policy-gated (likeapprove).
15. Metadata
METADATA_ROLE gates name, symbol, and contract URI updates:
16. Precompile Addresses
These addresses are identical on every network where B20 is active (Mainnet, Base Sepolia, Vibenet, and localbase-anvil).
17. Invariants
Policy Registry
isAuthorizednever reverts for any combination ofpolicyIdandaccount.- A non-existent
BLOCKLISTpolicy authorizes everyone. A non-existentALLOWLISTpolicy denies everyone. - After
renounceAdmin(policyId), all membership-mutating calls on that policy revert permanently. ALWAYS_ALLOW(ID0) authorizes every account.ALWAYS_BLOCKdenies every account. Neither can be created, modified, or renounced.- Policy IDs are globally unique and monotonically increasing within each
PolicyTypeprefix.
Roles
- The last
DEFAULT_ADMIN_ROLEholder cannot be removed viarenounceRoleorrevokeRole— onlyrenounceLastAdmin(). - After
renounceLastAdmin(), no address can ever holdDEFAULT_ADMIN_ROLEagain. - Roles granted before admin renunciation continue to function.
- Custom roles have no built-in effect on any B20 operation.
Transfer Policies
approveis never policy-gated.TRANSFER_EXECUTOR_POLICYis checked only ontransferFrom, never ontransfer.MINT_RECEIVER_POLICYis always enforced, even during factoryinitCalls.- All three transfer-side scopes are bypassed during
initCalls. - Every scope defaults to
ALWAYS_ALLOWat token creation.
Supply
totalSupplycan never exceed the supply cap.- The supply cap can never be set below the current
totalSupply. - Burns reduce
totalSupplyand create headroom under the cap.
Pause
- Each
PausableFeatureis independent — pausing one does not affect the others. approveandpermitare never affected by any pause state.- Pause state is never bypassed during factory
initCalls.
Memos
- The
Memoevent is always emitted at exactlylogIndex + 1relative to its parentTransferevent. - Memo methods are functionally identical to their non-memo counterparts in all respects except event emission.
Permit
permitonly accepts ECDSA signatures. ERC-1271 contract signatures always fail.- Each successful
permitincrements the owner’s nonce by exactly 1. - Permits signed before
updateNamefail after the name change.
Variants
- Asset decimals are set at creation and immutable. Valid range is 6–18.
- Stablecoin decimals are always
6. OPERATOR_ROLEexists only on Asset tokens.- Announcement IDs are unique across a token’s lifetime.
- The currency code on a Stablecoin is immutable and contains only
A–Zcharacters. - Multiplier updates affect all holders simultaneously.
batchMintenforcesMINT_RECEIVER_POLICYfor each recipient individually.
Factory
- B20 addresses are deterministic: same inputs always produce the same address.
- The variant byte at address position 10 always matches the deployed variant.
- Each
(deployer, variant, salt)tuple produces exactly one address. initCallsexecute in array order. A revert in any initCall reverts the entire deployment.