> ## Documentation Index
> Fetch the complete documentation index at: https://base-a060aa97-docs-devrel-859-eth-getproof-historical-proof.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Apply a Multiplier

> Schedule a B20 Asset multiplier update so displayed unit counts change at a future time without rewriting raw balances. A stock split is one example.

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](https://eips.ethereum.org/EIPS/eip-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.

<Note>
  Real-world asset (RWA) tokenization is one of many use cases for the [B20 Asset standard](/specifications/b20/concepts/token-types#4-asset). 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.
</Note>

<Warning>
  The routine path is `updateUIMultiplier`. The deprecated `updateMultiplier` applies a value immediately and clears any pending update. Use it only as an emergency override.
</Warning>

<Note>
  New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@1505323`, `viem@2.55.11`, and Base Foundry `v1.1.1`.
</Note>

## 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](/build-on-base/issue-rwa/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:

| Read | Formula |
| - | - |
| `uiMultiplier()` / `multiplier()` | Effective multiplier at `block.timestamp` |
| `balanceOfUI(account)` / `scaledBalanceOf(account)` | `balanceOf(account) * uiMultiplier() / 1e18` |
| `totalSupplyUI()` | `totalSupply() * uiMultiplier() / 1e18` |
| `toUIAmount(raw)` / `fromUIAmount(ui)` | Convert at the effective multiplier |

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:

```solidity Grant OPERATOR_ROLE theme={null}
asset.grantRole(asset.OPERATOR_ROLE(), operator);
```

Then schedule the update. This example schedules a 2-for-1 split for tomorrow:

<CodeGroup>
  ```typescript TypeScript lines wrap expandable highlight={8} theme={null}
  import { type Address } from "viem";
  import { publicClient } from "../../shared/clients.js";
  import { assetAbi } from "../abi.js";
  import { sendContract } from "../write.js";

  export async function scheduleTwoForOneSplit(token: Address) {
    const effectiveAt = BigInt(Math.floor(Date.now() / 1000) + 86_400);
    await sendContract({ address: token, abi: assetAbi, functionName: "updateUIMultiplier", args: [2n * 10n ** 18n, effectiveAt] });
    const pending = await publicClient.readContract({ address: token, abi: assetAbi, functionName: "newUIMultiplier" });
    if (pending !== 2n * 10n ** 18n) throw new Error("Split was not scheduled");
    return effectiveAt;
  }
  ```

  ```solidity Solidity lines wrap expandable highlight={3} theme={null}
  function scheduleSplit(address token) public returns (uint256 effectiveAt) {
      effectiveAt = block.timestamp + 1 days;
      IB20Asset(token).updateUIMultiplier(2e18, effectiveAt);
      require(IB20Asset(token).newUIMultiplier() == 2e18, "split not scheduled");
      require(IB20Asset(token).effectiveAt() == effectiveAt, "effectiveAt mismatch");
  }
  ```

  ```bash CLI lines wrap expandable highlight={2} theme={null}
  EFFECTIVE_AT=$(( $(date +%s) + 86400 ))
  base-cast send "$TOKEN_ADDRESS" "updateUIMultiplier(uint256,uint256)" 2000000000000000000 "$EFFECTIVE_AT" \
    --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY"
  base-cast call "$TOKEN_ADDRESS" "newUIMultiplier()(uint256)" --rpc-url "$RPC_URL"
  ```
</CodeGroup>

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

```solidity Read the pending state theme={null}
asset.uiMultiplier();     // still the current multiplier
asset.newUIMultiplier();  // scheduled target
asset.effectiveAt();      // flip timestamp
```

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.

<Check>
  `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.
</Check>

To disclose the schedule to holders and indexers in the same transaction, wrap the call in `announce`; see [Announce a Change to Holders](/build-on-base/issue-rwa/announce-a-distribution).

## 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`:

```solidity Cancel the pending update theme={null}
asset.cancelUIMultiplierUpdate();
```

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`:

```solidity Cancel and reschedule in one announcement wrap theme={null}
bytes[] memory calls = new bytes[](2);
calls[0] = abi.encodeCall(IB20Asset.cancelUIMultiplierUpdate, ());
calls[1] = abi.encodeCall(IB20Asset.updateUIMultiplier, (secondMultiplier, secondEffectiveAt));
asset.announce(calls, "2026-multiplier-02", "Rescheduled multiplier update", "https://example.com/announcements/2026-multiplier-02");
```

<Note>
  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.
</Note>

## 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.

| Situation | Events (in order) |
| - | - |
| Live pending (`effectiveAt > block.timestamp`) | `UIMultiplierUpdateCancelled`, then `MultiplierUpdated(new)`, then `UIMultiplierUpdated(old, new, block.timestamp)` |
| Matured or no pending | `MultiplierUpdated(new)`, then `UIMultiplierUpdated(old, new, block.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.

| Error | Cause | Fix |
| - | - | - |
| `AccessControlUnauthorizedAccount(caller, OPERATOR_ROLE)` | Caller lacks `OPERATOR_ROLE`. | Grant `OPERATOR_ROLE` to the operator. |
| `InvalidMultiplier()` | `newMultiplier` is zero or above `MAX_UI_MULTIPLIER`. | Pass a value in `(0, MAX_UI_MULTIPLIER]`. |
| `EffectiveAtInPast(effectiveAt)` | `effectiveAt <= block.timestamp`. | Pass a strictly future timestamp. |
| `EffectiveAtTooFar(effectiveAt)` | `effectiveAt > type(uint64).max`. | Pass a timestamp that fits in `uint64`. |
| `UIMultiplierUpdateExists(effectiveAt)` | A live pending update already exists. | Cancel first, or cancel and reschedule in one `announce`. |
| `UIMultiplierUpdateDoesNotExist()` | `cancelUIMultiplierUpdate` with no live pending update, including after maturity. | Call only while `effectiveAt() > block.timestamp`. |
| `AnnouncementIdAlreadyUsed(id)` | `announce` reused an `id`. | Choose a new single-use `id`. |
| `InternalCallFailed(call)` | An inner call in `announce` reverted. | Fix the encoded calldata and retry. |

## See Also

<CardGroup cols={2}>
  <Card title="Announce a Change to Holders" icon="bullhorn" href="/build-on-base/issue-rwa/announce-a-distribution">
    Disclose the update to holders and indexers in the same transaction.
  </Card>

  <Card title="Pause Transfers" icon="circle-pause" href="/build-on-base/issue-rwa/pause-transfers">
    Halt transfers in an emergency.
  </Card>
</CardGroup>
