B20, and how LUSG uses it.
B20 is Base's native token standard for stablecoins and real-world assets. A token is created by one call to a factory precompile; there is no token contract to write or audit. It is ERC-20 compatible, with roles, policies, pause, supply caps, memos and, for assets, an ERC-8056 multiplier built in.
Precompiles
| Precompile | Address | Role |
|---|---|---|
| B20 factory | 0xB20f000000000000000000000000000000000000 | createB20(variant, salt, params, initCalls); deterministic addresses starting 0xB200… |
| Policy Registry | 0x8453000000000000000000000000000000000002 | Allowlists, blocklists and combined policies, shared across tokens |
| Activation Registry | 0x8453000000000000000000000000000000000001 | Base's switch for which variants and features are live (all three are on for Vibenet and Base Sepolia) |
Upgrade timeline
What LUSG uses
| B20 feature | LUSG's use |
|---|---|
| Asset variant, 18 decimals | The token. 18 decimals keeps the multiplier's rounding at one unit in the last place. |
initCalls at creation | Every role, the cap and the policy bindings were set in the single createB20 transaction (0x1f4866c0…be5a →). |
Roles (MINT, BURN, PAUSE, UNPAUSE, SEIZE, METADATA, OPERATOR) | Split across wallets and contracts; see who holds what. |
| ERC-8056 UI multiplier | Carries the yield. updateUIMultiplier(m, effectiveAt) is scheduled every minute by LusgClock. balanceOf stays raw; balanceOfUI shows dollars. |
| Policy scopes | One combined policy (KYC allowlist and not sanctions-blocklisted) on TRANSFER_SENDER, TRANSFER_RECEIVER and MINT_RECEIVER. |
SEIZE_EXEMPT_POLICY | Bound to a treasury-only list. It defaults to always-allow, which exempts everyone; see compliance. |
| Pause by feature | TRANSFER, MINT, BURN, SEIZE pause independently. Base Sepolia adds an auto-pause of mint and redeem on a market gap. |
| Supply cap | 100,000,000 LUSG on Vibenet (raised from 6,000,000 when the desk opened). |
Memos (mintWithMemo, seizeWithMemo) | Each mint and seizure carries a reference for reconciliation, for example a custodian settlement ID. |
The multiplier, precisely
- Raw and UI. UI balance = raw × multiplier / 1e18. Transfers, allowances and
totalSupplystay in raw units. - Absolute values. A multiplier is a level, not a ratio: LUSG's is the basket's dollar value per raw token (1e18 = $1.00).
- One pending update.
updateUIMultiplierreverts withUIMultiplierUpdateExistswhile a scheduled value is still in the future; once it takes effect, the next one may be scheduled. No minimum notice and no bound on the size of the move. - No event at the flip.
UIMultiplierUpdated(old, new, effectiveAt)fires when an update is scheduled. Indexers should read state at a timestamp.
Creating LUSG
params = abi.encode((uint8 version = 1, string name, string symbol, address initialAdmin, uint8 decimals = 18))
initCalls = [ grantRole(MINT_ROLE, minter), grantRole(BURN_ROLE, minter),
grantRole(PAUSE_ROLE, compliance), grantRole(UNPAUSE_ROLE, compliance), grantRole(SEIZE_ROLE, compliance),
grantRole(METADATA_ROLE, admin), grantRole(OPERATOR_ROLE, clock),
updateSupplyCap(cap),
updatePolicy(TRANSFER_SENDER_POLICY, holderPolicy), updatePolicy(TRANSFER_RECEIVER_POLICY, holderPolicy),
updatePolicy(MINT_RECEIVER_POLICY, holderPolicy), updatePolicy(SEIZE_EXEMPT_POLICY, exemptList) ]
factory.createB20(ASSET = 0, salt, params, initCalls) // simulate with eth_call first: it returns the address
The token address is deterministic from (variant, sender, salt), so LusgClock was deployed against the predicted address first and received OPERATOR_ROLE inside the same createB20 call.