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.
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 grantOPERATOR_ROLE. - An account that will call the multiplier setters (the operator).
updateUIMultiplier,cancelUIMultiplierUpdate, and the deprecatedupdateMultiplierall requireOPERATOR_ROLE. The token created in Create an Asset Token grants it to the deployer. - A future
effectiveAttimestamp and anewMultiplierin(0, MAX_UI_MULTIPLIER].
PausableFeature freezes only TRANSFER, MINT, BURN, and SEIZE.
What a Multiplier Changes
RawbalanceOf 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.- Schedule.
updateUIMultiplier(newMultiplier, effectiveAt)stores the pending pair.effectiveAtmust be strictly greater thanblock.timestamp. - Live pending. While
effectiveAt() > block.timestamp,uiMultiplier()still returns the current value,newUIMultiplier()returns the scheduled value, andeffectiveAt()returns the flip time. A second schedule revertsUIMultiplierUpdateExists. - Maturation. When
block.timestamp >= effectiveAt, reads return the new multiplier. Maturation writes no storage and emits no event. - After maturity. Until another update,
newUIMultiplier()mirrorsuiMultiplier()andeffectiveAt()keeps the past timestamp. Detect a live pending update witheffectiveAt() > block.timestamp, never witheffectiveAt() == 0.
Schedule and Verify the Multiplier Update
GrantOPERATOR_ROLE once per token if the operator does not already hold it:
Grant OPERATOR_ROLE
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
updateUIMultiplier while a pending update is live reverts UIMultiplierUpdateExists. To replace a pending update, cancel first, then reschedule.
Confirm After the Effective Time
Whenblock.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.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 beforeeffectiveAt:
Cancel the pending update
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
UseupdateMultiplier(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 deprecatedMultiplierUpdated. - 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 checkeffectiveAt() == 0.
Common Errors
These errors follow the orderupdateUIMultiplier 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.