Skip to main content
A multiplier rescales what holders see without touching what they hold. At the agreed time, every displayed balance changes by the same factor while raw balanceOf, totalSupply, and transfer amounts stay the same, so DeFi that reads raw units keeps working. Exchanges, custodians, wallets, and accounting systems need that time in advance, so you schedule the update onchain ahead of it with updateUIMultiplier (ERC-8056). This guide uses a 2-for-1 stock split as its worked example; a reverse split or a reinvested distribution follows the same path with a different value.
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.
The routine path is updateUIMultiplier. The deprecated updateMultiplier applies a value immediately and clears any pending update. Use it only as an emergency override.

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 OPERATOR_ROLE.
  • An account that will call the multiplier setters (the operator). updateUIMultiplier, cancelUIMultiplierUpdate, and the deprecated updateMultiplier all require OPERATOR_ROLE. The token created in Create an Asset Token grants it to the deployer.
  • A future effectiveAt timestamp and a newMultiplier in (0, MAX_UI_MULTIPLIER].
This surface exists only on B20 Asset. Stablecoin has no multiplier. Pause does not gate the setters: PausableFeature freezes only TRANSFER, MINT, BURN, and SEIZE.

What a Multiplier Changes

Raw balanceOf is unchanged by a multiplier. Only the derived UI views apply it: The multiplier is an 18-decimal WAD: 1e18 is 1.0. A 2-for-1 split uses 2e18. A 1-for-2 reverse split uses 5e17. Integer division rounds down, so the rounding loss is confined to the scaled view and is at most one unit of the scaled amount. The raw-side difference on a round trip through fromUIAmount can be larger when the multiplier is below 1e18. Prefer 18 decimals for stock tokens to keep that effect small.

How a Schedule Lives

The asset allows one pending multiplier at a time.
  1. Schedule. updateUIMultiplier(newMultiplier, effectiveAt) stores the pending pair. effectiveAt must be strictly greater than block.timestamp.
  2. Live pending. While effectiveAt() > block.timestamp, uiMultiplier() still returns the current value, newUIMultiplier() returns the scheduled value, and effectiveAt() returns the flip time. A second schedule reverts UIMultiplierUpdateExists.
  3. Maturation. When block.timestamp >= effectiveAt, reads return the new multiplier. Maturation writes no storage and emits no event.
  4. After maturity. Until another update, newUIMultiplier() mirrors uiMultiplier() and effectiveAt() keeps the past timestamp. Detect a live pending update with effectiveAt() > block.timestamp, never with effectiveAt() == 0.

Schedule and Verify the Multiplier Update

Grant OPERATOR_ROLE once per token if the operator does not already hold it:
Grant OPERATOR_ROLE
Then schedule the update. This example schedules a 2-for-1 split for tomorrow:
On success, the asset emits UIMultiplierUpdated(oldMultiplier, newMultiplier, effectiveAtTimestamp). That event fires when the update is recorded, not when the multiplier becomes active. Read MAX_UI_MULTIPLIER() if you need the ceiling without triggering InvalidMultiplier.

Read the Live Pending State

Read the pending state
A second updateUIMultiplier while a pending update is live reverts UIMultiplierUpdateExists. To replace a pending update, cancel first, then reschedule.

Confirm After the Effective Time

When block.timestamp >= effectiveAt, uiMultiplier() returns the new multiplier. Maturation emits no event. Do not wait for a second event at the flip.
UIMultiplierUpdated appears on the schedule transaction. While pending, uiMultiplier() equals the old value and newUIMultiplier() equals 2e18. At or after effectiveAt, uiMultiplier() returns 2e18, raw balanceOf is unchanged, and balanceOfUI doubles.
To disclose the schedule to holders and indexers in the same transaction, wrap the call in announce; see Announce a Change to Holders.

Cancel a Pending Update

Use this when a pending update should not take effect: wrong multiplier, wrong timestamp, or the action is delayed. Call before effectiveAt:
Cancel the pending update
Emits UIMultiplierUpdateCancelled(cancelledMultiplier, cancelledEffectiveAt). uiMultiplier() stays at the old value. Calling with no live pending update, including after maturity, reverts UIMultiplierUpdateDoesNotExist. To cancel and reschedule atomically, wrap both calls in announce:
Cancel and reschedule in one announcement
Pausing TRANSFER around a multiplier update is possible but not recommended for routine updates. It stops every holder transfer for the window, which is heavier than most changes need, and requires PAUSE_ROLE and UNPAUSE_ROLE in addition to the operator. Prefer the schedule and let wallets and custodians coordinate off the pending state.

Emergency Override

Use updateMultiplier(newMultiplier) only when a pending update is wrong and you cannot wait for effectiveAt. It applies the value immediately and clears any pending update. Prefer cancel when waiting is acceptable, and cancel-then-reschedule when the fix is still a future timestamp. MultiplierUpdated is deprecated. Process only UIMultiplierUpdated to avoid handling the same update twice.

Integrator Rules

  • Listen for UIMultiplierUpdated, not the deprecated MultiplierUpdated.
  • If effectiveAtTimestamp > block.timestamp, treat the update as pending until that time.
  • Maturation emits nothing. Do not wait for a second event at the flip.
  • On UIMultiplierUpdateCancelled, discard the pending update.
  • Detect a live pending update with effectiveAt() > block.timestamp. Do not check effectiveAt() == 0.

Common Errors

These errors follow the order updateUIMultiplier checks them. Cancel and announce errors follow.

See Also

Announce a Change to Holders

Disclose the update to holders and indexers in the same transaction.

Pause Transfers

Halt transfers in an emergency.