updateUIMultiplier, for corporate actions such as stock splits and reinvested dividends. Nothing you call on Beryl breaks. Every Beryl selector, event topic0, and error keeps its exact bytes and stays dialable at Cobalt.
Audience and compatibility promise
This page is for teams already integrated against the Beryl B20 Asset multiplier surface (multiplier, scaledBalanceOf, toScaledBalance, toRawBalance, updateMultiplier, MultiplierUpdated).
The migration is additive:
- Every Beryl symbol keeps its exact 4-byte selector or topic0 at Cobalt and remains dialable. The
deprecated-*labels below are advisory. - Adopt the canonical ERC-8056 names (
uiMultiplier,toUIAmount/fromUIAmount,balanceOfUI,totalSupplyUI) at your own pace. - Move routine multiplier changes from the instant
updateMultiplier(uint256)to the scheduledupdateUIMultiplier(uint256,uint256). Keep the instant setter for emergency overrides only.
transfer semantics, and the Transfer event are unchanged. The multiplier is cosmetic — it rescales only the *UI / scaled views.
Symbol mapping
Selectors and topic0s are the canonical values from the ERC-8056 interfaces the B20 Asset precompile implements at Cobalt. Every Beryl symbol keeps the selector or topic0 it has today.Functions
OPERATOR_ROLE(), WAD_PRECISION(), announce, isAnnouncementIdUsed, batchMint, extraMetadata, and updateExtraMetadata are carried over unchanged.
Events
Errors
New at Cobalt (adopt these)
Scheduled-update lifecycle
updateUIMultiplier(newMultiplier, effectiveAt) is the canonical corporate-action path. It requires OPERATOR_ROLE. Only one pending update is live at a time.
- Schedule. Call
updateUIMultiplier(newMultiplier, effectiveAt).effectiveAtmust be strictly in the future and must fituint64. - Pending views. While a pending update is live,
newUIMultiplier()returns the scheduled target,effectiveAt()returns the flip timestamp, anduiMultiplier()/multiplier()still return the current value. - Matures lazily. Once
block.timestamp >= effectiveAt,uiMultiplier()andmultiplier()flip on the next read. No event fires at maturation. - Cancel.
cancelUIMultiplierUpdate()clears a live pending and emitsUIMultiplierUpdateCancelled(cancelledMultiplier, cancelledEffectiveAt).
ERC-8056 view aliases
Feature-detect the new surface with ERC-165:
Multiplier ceiling
MAX_UI_MULTIPLIER() returns type(uint128).max. This is the ceiling both setters enforce — the overflow guard that keeps balance * multiplier inside uint256. Read it instead of hard-coding the constant, and validate off-chain to avoid the InvalidMultiplier() revert.
Instant updateMultiplier retained as emergency failsafe
updateMultiplier(uint256) still exists at Cobalt and sets the multiplier immediately. If a live pending update exists, the instant setter clears it. It is kept for emergency overrides only, not routine use — pair it with pause in most cases. When it fires, it emits both the legacy MultiplierUpdated(uint256) and the ERC-8056 UIMultiplierUpdated(old, new, block.timestamp), so a single subscription to UIMultiplierUpdated covers every multiplier change.
Edge cases and guarantees
How do external consumers detect a cancellation?cancelUIMultiplierUpdate() emits UIMultiplierUpdateCancelled(cancelledMultiplier, cancelledEffectiveAt). The instant updateMultiplier(uint256) emits the same event when it supersedes a live pending. Watch that topic to retract a pending flip you previously staged from UIMultiplierUpdated.
How do off-chain indexers keep a linear, gap-free lifecycle when the emergency failsafe fires?
Every multiplier change — scheduled or emergency — appears on the UIMultiplierUpdated stream. The instant setter emits both MultiplierUpdated(uint256) and UIMultiplierUpdated(old, new, block.timestamp), and emits UIMultiplierUpdateCancelled first if it clears a live pending. Follow the single UIMultiplierUpdated topic and you never miss a change. The legacy MultiplierUpdated topic remains available for indexers that have not migrated.
How do I tell a live pending update apart from one that has already matured?
A pending is live if effectiveAt() > block.timestamp. While live, newUIMultiplier() returns the scheduled target and differs from uiMultiplier(). After maturation, uiMultiplier() already reflects the new value and newUIMultiplier() == uiMultiplier(). effectiveAt() stays at the (now past) flip timestamp until the next schedule, instant, or cancel overwrites it. A non-zero effectiveAt() that is <= block.timestamp means “already applied,” not “pending.” When no update has ever been scheduled, effectiveAt() == 0.
What if I schedule while one is already pending?
Reverts UIMultiplierUpdateExists(effectiveAt), but only a live pending blocks. A matured (stale) pending is silently folded into the current multiplier and overwritten. To replace a live schedule, call cancelUIMultiplierUpdate() and then updateUIMultiplier(...) atomically via announce.
What are the effectiveAt bounds?
effectiveAt must be strictly in the future. effectiveAt <= block.timestamp reverts EffectiveAtInPast(effectiveAt). It must also fit the on-chain field: effectiveAt > type(uint64).max reverts EffectiveAtTooFar(effectiveAt).
What are the multiplier bounds?
0 < newMultiplier <= MAX_UI_MULTIPLIER() (type(uint128).max). Zero or above reverts InvalidMultiplier(). This applies to both updateUIMultiplier and updateMultiplier. Read the ceiling from MAX_UI_MULTIPLIER() without risking the revert.
Do raw balances or Transfer semantics change?
No. The multiplier is cosmetic and rescales only the *UI / scaled views. balanceOf, transfer, totalSupply, and Transfer stay raw and are mechanically unaffected by any multiplier change, scheduled or instant.