Beryl / B20
B20 is Base's native standard for compliant tokenized assets — enforced by a Rust precompile in the node, not EVM bytecode. Activated by the Beryl upgrade on June 25, 2026.
What is B20?
B20 (Base Standard 20) is a tokenization primitive built directly into the Base node as a Rust precompile. This means compliance rules — pause, policy gating, supply caps — are enforced at the node level and cannot be bypassed by EVM bytecode.
B20 is not a smart contract standard like ERC-20. It is an extension of the Base execution environment itself, activated by the Beryl network upgrade.
Two Variants
multiplier() view function.currency() field (e.g. "USD") to declare the peg.Key Addresses
All precompile addresses are the same on both mainnet and Sepolia.
B20Factory 0xB20f000000000000000000000000000000000000 PolicyRegistry 0x8453000000000000000000000000000000000002 ActivationRegistry 0x8453000000000000000000000000000000000001
Deploying a B20 Token
Tokens are created through the B20Factory. The factory emits a B20Created event for each deploy.
// ASSET variant (variant = 0) IB20Factory factory = IB20Factory(0xB20f000000000000000000000000000000000000); address token = factory.createB20( "My Token", // name "MTK", // symbol 18, // decimals 0, // variant: 0 = ASSET, 1 = STABLECOIN "" // variantParams (abi-encoded extra data) );
Policy System
B20 supports exactly two policy types: ALLOWLIST and BLOCKLIST. Policies are managed through the PolicyRegistry precompile and assigned to one of four scopes per token.
TRANSFER_SENDER_POLICYGoverns who can send tokens.TRANSFER_RECEIVER_POLICYGoverns who can receive tokens.TRANSFER_EXECUTOR_POLICYGoverns who can call transferFrom().MINT_RECEIVER_POLICYGoverns who can receive newly minted tokens.policyId = 0 means ALWAYS_ALLOW (no restriction). To restrict a scope:
IPolicyRegistry pReg = IPolicyRegistry(0x8453000000000000000000000000000000000002); // 1. Create the policy (returns a uint64 policyId) uint64 id = pReg.createPolicy(adminAddress, IPolicyRegistry.PolicyType.ALLOWLIST); // 2. Add authorized addresses pReg.addToPolicy(id, allowedAddress); // 3. Apply it to the token scope token.updatePolicy(token.TRANSFER_RECEIVER_POLICY(), id);
createPolicy(admin, PolicyType) on the PolicyRegistry to get a policyId, then apply it with token.updatePolicy(scope, policyId). Freeze-seize (burnBlocked()) and supply caps (updateSupplyCap()) are role-gated, not policy types.7 Core Roles
B20 uses OpenZeppelin AccessControl internally but omits AccessControlEnumerable — role holders cannot be enumerated. Use hasRole(role, address) to check specific accounts (see the B20 Hub → Roles tab).
DEFAULT_ADMIN_ROLEManages all other roles. Sets supply cap via updateSupplyCap().MINT_ROLECan mint new tokens to any address.BURN_ROLECan burn tokens (holder must approve or consent).BURN_BLOCKED_ROLEFreeze-seize: can call burnBlocked(from, amount) to confiscate and burn tokens.PAUSE_ROLECan pause TRANSFER, MINT, or BURN features independently.UNPAUSE_ROLECan unpause any paused feature.METADATA_ROLECan update token name, symbol, and other metadata.Supply Cap
B20 tokens have an on-chain supply cap enforced at the node level. The sentinel value type(uint128).max means uncapped. Call token.supplyCap() to read the current cap. Update it with token.updateSupplyCap(newCap) — requires DEFAULT_ADMIN_ROLE.
Pause / Unpause
Transfers, minting, and burns can each be paused independently. Call token.isPaused(PausableFeature.TRANSFER) to check. The Scanner tab in the B20 Hub shows live pause status for any token.
Freeze-Seize (burnBlocked)
The BURN_BLOCKED_ROLE grants the ability to forcibly confiscate and burn tokens from any address. This is distinct from policy blocking (which prevents transfers) — burnBlocked permanently removes tokens from circulation. It requires explicit regulatory authority and is designed for asset recovery in regulated markets.
// Requires BURN_BLOCKED_ROLE token.burnBlocked(holderAddress, amount);
On-Chain Inspector
Use the B20 Hub to inspect any token live — zero LLM, all data from multicall. Check scanner results, role holders, the on-chain registry, and simulate transfers before they happen.
deploy a B20 asset token named "My Token" symbol "MTK" — the agent will generate and sign a createB20 Factory transaction directly.How the B20 scanner works
Every result in the B20 Hub Scanner is read live from Base RPC via multicall — zero LLM, zero guessing. The numbers and flags come straight from on-chain state, never from a model.
Inspection flow
The scanner reads each token in a fixed, deterministic sequence:
- 1.Validate the address format.
- 2.Check isB20 against the B20Factory precompile — confirm it is a real B20, not arbitrary EVM bytecode.
- 3.Read core state: name, symbol, decimals, total supply, supply cap, and variant.
- 4.Read pause status per feature: transfer, mint, and burn.
- 5.Read the policy ID per scope: transfer sender, transfer receiver, transfer executor, and mint receiver.
- 6.Read variant detail: the rebase multiplier (Asset) or the currency code (Stablecoin).
Trust verdict — deterministic, not a score
We surface concrete flags, never a single pass/fail number that would overstate certainty. The verdict is computed in code from the reads above, so the same on-chain state always yields the same flags.
Limitations
- —B20 omits AccessControlEnumerable, so role holders cannot be listed — each role is only checked per wallet via hasRole.
- —Reads reflect on-chain state at the moment of the scan. Roles and policies can change afterward.
- —Advisory only — verify independently before trusting or trading a token.