Skip to main content
Issuers of a B20 Asset need a paved way to disclose a change that affects holders, and indexers need a reliable way to tell which changes were announced. 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

The demo uses a local browser-generated account to submit real transactions on Base Vibenet. If Vibenet or its B20 features are unavailable, it automatically switches to an illustrative offline version.
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_ROLE so you can grant roles.
  • An account that will call announce (the operator). Grant it OPERATOR_ROLE.
  • Any role the wrapped calls need, granted to the same operator. batchMint and mintWithMemo need MINT_ROLE; burnWithMemo needs BURN_ROLE; the multiplier setters already use OPERATOR_ROLE.
  • A never-used id, a holder-facing description, and optionally a uri to the full offchain record.
This surface exists only on B20 Asset. Stablecoin has no announcements.

How an Announcement Works

announce(internalCalls, id, description, uri) runs in this order:
  1. Emit Announcement(caller, id, description, uri).
  2. Run every entry in internalCalls atomically. 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 as msg.sender.
  3. Emit EndAnnouncement(id).
If any inner call fails, the whole transaction reverts and 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
Then encode the inner call and announce it. This example wraps batchMint to issue a stock dividend in additional units:
Recipients must pass 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

Wrap updateUIMultiplier(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
On success the asset emits 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

Wrap burnWithMemo(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
On success the asset emits 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 empty internalCalls 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 order announce 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.