announce combines the state change and the disclosure in one transaction: it emits Announcement before the inner calls and EndAnnouncement after them. This guide uses a stock dividend as its worked example; the same bracket applies to any operator-driven change on a B20 Asset, including multiplier updates, treasury burns, and notices with no onchain effect.
Real-world asset (RWA) tokenization is one of many use cases for the B20 Asset standard. The examples on this page use a stock token for illustration; the same flows apply to other asset types.Tokenized securities examples shown for illustration. Base is a general-purpose blockchain; issuance and compliance are the responsibility of the issuer under applicable law.
Demo
New to B20? See the B20 Token Standard for the concepts and a full launch walkthrough. These samples target
base-std@1505323, viem@2.55.11, and Base Foundry v1.1.1.Before You Start
You need all of the following:- A B20 Asset you administer, with
DEFAULT_ADMIN_ROLEso you can grant roles. - An account that will call
announce(the operator). Grant itOPERATOR_ROLE. - Any role the wrapped calls need, granted to the same operator.
batchMintandmintWithMemoneedMINT_ROLE;burnWithMemoneedsBURN_ROLE; the multiplier setters already useOPERATOR_ROLE. - A never-used
id, a holder-facingdescription, and optionally aurito the full offchain record.
How an Announcement Works
announce(internalCalls, id, description, uri) runs in this order:
- Emit
Announcement(caller, id, description, uri). - Run every entry in
internalCallsatomically. Each entry is ABI-encoded calldata against this asset (at least 4 bytes), not a(target, calldata)pair. Inner calls keep their own role gates and run with the operator asmsg.sender. - Emit
EndAnnouncement(id).
id stays free. A successful announce consumes id for the asset’s lifetime; isAnnouncementIdUsed(id) then returns true. description and uri are operator-supplied and not verified by the asset. Nesting announce inside an inner call reverts AnnouncementInProgress.
For indexers: pair Announcement and EndAnnouncement by id, not only by adjacency. Every effect between the two logs belongs to the announced action. A state change that is not inside a bracket was invoked directly, not through announce.
Announce and Distribute Additional Units
Grant the roles once per token:Grant OPERATOR_ROLE and MINT_ROLE
batchMint to issue a stock dividend in additional units:
MINT_RECEIVER_POLICY, MINT must not be paused, and batchMint is all-or-nothing. On success the asset emits Announcement, then one Transfer(address(0), recipient, amount) per recipient, then EndAnnouncement. With mintWithMemo instead of batchMint, the order is Announcement, Transfer, Memo, EndAnnouncement.
Announcement and EndAnnouncement carry the same id, the inner Transfer events sit between them, and isAnnouncementIdUsed(id) returns true.This example issues additional units. It does not distribute cash. A reinvested dividend that only rescales displayed balances is a multiplier update; see the next section and Apply a Multiplier.
Announce Other Changes
The same bracket discloses any operator-driven change. Each scenario lists the roles the operator needs and the events the asset emits.Multiplier Update
WrapupdateUIMultiplier(newMultiplier, effectiveAt). A 2-for-1 split uses 2e18; a reverse split uses a value below 1e18. The operator needs OPERATOR_ROLE only.
Announce a multiplier update
Announcement, UIMultiplierUpdated, then EndAnnouncement. UIMultiplierUpdated means the schedule was recorded, not that the multiplier is already active. A direct updateUIMultiplier with no announce still works; indexers should flag it as undisclosed.
To replace a live pending update, cancel and reschedule in one announce so both calls share the operator:
Cancel and reschedule in one announcement
Treasury Burn
WrapburnWithMemo(amount, memo). The call burns the operator’s own balance and is not policy-gated. The operator needs OPERATOR_ROLE and BURN_ROLE. BURN must not be paused.
Announce a treasury burn
Announcement, Transfer(operator, address(0), amount), Memo, then EndAnnouncement. totalSupply decreases. Do not use the deprecated burnBlocked. To take units from a holder and then destroy them, seize first, then announce the burn from the treasury.
Notice With No Onchain Effect
Pass an emptyinternalCalls array. The operator needs OPERATOR_ROLE only. The asset emits Announcement then EndAnnouncement with nothing between them. The id is still consumed.
Notice only
Common Errors
These errors follow the orderannounce checks them. Inner-call failures follow.
Typical inner causes of
InternalCallFailed: missing MINT_ROLE or BURN_ROLE, paused MINT or BURN, UIMultiplierUpdateExists, PolicyForbids, SupplyCapExceeded, InsufficientBalance (on burn, against the operator’s own balance). A Solidity Panic such as overflow propagates raw and is not wrapped as InternalCallFailed.
See Also
Apply a Multiplier
Schedule, cancel, or override a multiplier update.
Issue Units to Holders
Mint asset units to holders.