Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions docs/build-on-base/integrate-defi/list-tokenized-stocks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,17 @@ Multiplier updates come in two forms:
- **Instant (deprecated):** [`updateMultiplier`](/specifications/b20/reference/interfaces/ib20-asset/update-multiplier) applies immediately and clears any pending scheduled change. It is a retained emergency failsafe; prefer the scheduled path.
- **Cancellation:** clear a pending scheduled update with [`cancelUIMultiplierUpdate`](/specifications/b20/reference/interfaces/ib20-asset/cancel-ui-multiplier-update).

The B20 contract provides several helper functions for common calculations, where `raw` is the number of B20 units and `scaled` is the quantity of stocks redeemable:
The B20 contract provides ERC-8056 helper functions for common calculations, where `raw` is the number of B20 units and `ui` is the quantity of stocks redeemable:

| Function | Description |
|----------|-------------|
| [`scaledBalanceOf(account)`](/specifications/b20/reference/interfaces/ib20-asset/scaled-balance-of) | Raw balance × multiplier |
| [`toScaledBalance(raw)`](/specifications/b20/reference/interfaces/ib20-asset/to-scaled-balance) | Convert raw amount to scaled |
| [`toRawBalance(scaled)`](/specifications/b20/reference/interfaces/ib20-asset/to-raw-balance) | Convert scaled amount to raw |
| [`uiMultiplier()`](/specifications/b20/reference/interfaces/ib20-asset/ui-multiplier) | Current effective multiplier (WAD) |
| [`balanceOfUI(account)`](/specifications/b20/reference/interfaces/ib20-asset/balance-of-ui) | Raw balance × multiplier |
| [`totalSupplyUI()`](/specifications/b20/reference/interfaces/ib20-asset/total-supply-ui) | Raw total supply × multiplier |
| [`toUIAmount(raw)`](/specifications/b20/reference/interfaces/ib20-asset/to-ui-amount) | Convert raw amount to UI amount |
| [`fromUIAmount(ui)`](/specifications/b20/reference/interfaces/ib20-asset/from-ui-amount) | Convert UI amount to raw |

`multiplier()` and `scaledBalanceOf(account)` remain the canonical B20 names; `uiMultiplier()` and `balanceOfUI(account)` are their ERC-8056 aliases and return the same values. `toScaledBalance` and `toRawBalance` remain callable but are deprecated in favor of `toUIAmount` and `fromUIAmount`. See the [Cobalt multiplier changelog](/base-chain/specs/reference/b20/changelog/02-cobalt-b20asset-multiplier#functions).

### Policies

Expand All @@ -75,7 +79,7 @@ Two design points:
- Announcements can be atomically bundled with the token change they describe (for example, the multiplier update for a stock split), keeping onchain records clean.
- Descriptions are intentionally human-readable onchain to support public reporting requirements.

**Admin actions:** admin operations and `updateMultiplier` ([`OPERATOR_ROLE`](/specifications/b20/reference/interfaces/ib20-asset/operator-role)) execute immediately when the role holder calls; the B20 standard has no built-in timelock. The `Announcement` and `EndAnnouncement` events are public notice, not an enforced delay. Any timelock or multisig is applied by the issuer at the governance layer.
**Admin actions:** admin operations and multiplier updates ([`OPERATOR_ROLE`](/specifications/b20/reference/interfaces/ib20-asset/operator-role)) execute when the role holder calls (scheduled multiplier updates flip at `effectiveAt`); the B20 standard has no built-in timelock. The `Announcement` and `EndAnnouncement` events are public notice, not an enforced delay. Any timelock or multisig is applied by the issuer at the governance layer.

### Extra Metadata

Expand Down Expand Up @@ -164,7 +168,7 @@ Two common patterns are used to determine value:

For historical OHLC and time-series data, use market-data providers or query Chainlink round history by `roundId`.

Since prices are total-return (multiplier-adjusted), reconstruct the series consistently by applying the multiplier history ([`MultiplierUpdated`](/specifications/b20/reference/interfaces/ib20-asset) events, emitted by both scheduled and instant multiplier changes) if starting from raw share prices.
Since prices are total-return (multiplier-adjusted), reconstruct the series consistently by applying the multiplier history if starting from raw share prices. Build that history from [`UIMultiplierUpdated`](/specifications/b20/reference/interfaces/ib20-asset) events, emitted by both scheduled and instant multiplier changes, and date each change by its `effectiveAtTimestamp`, not the block time. Drop any scheduled update later cancelled by a `UIMultiplierUpdateCancelled` event: it emitted `UIMultiplierUpdated` but never took effect.

## Contract Addresses

Expand Down
28 changes: 16 additions & 12 deletions docs/snippets/AssetDemo.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const AssetDemo = ({ flow }) => {
const moduleUrl = (source) => URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
const [aaSource, engineSource] = await Promise.all([
fetchText("/static/aa.txt"),
fetchText("/static/vibenet-engine.txt?v=4"),
fetchText("/static/vibenet-engine.txt?v=5"),
]);
const aaUrl = moduleUrl(aaSource);
const rewritten = engineSource.replace('"./aa.txt"', JSON.stringify(aaUrl));
Expand Down Expand Up @@ -214,11 +214,11 @@ export const AssetDemo = ({ flow }) => {
{ stage: "Load", action: "Load balances",
text: "Alice holds 100 raw shares and Bob holds 50.",
summary: [["Operation", "Load balances"], ["Multiplier", M("1.0 WAD")], ["Holders", "Alice, Bob"]],
run: (s) => { s.balances.Alice = 100; s.balances.Bob = 50; return { entries: [nfo("multiplier()", "1.0 WAD")], caption: "Raw balances and displayed balances currently match." }; } },
run: (s) => { s.balances.Alice = 100; s.balances.Bob = 50; return { entries: [nfo("uiMultiplier()", "1.0 WAD")], caption: "Raw balances and displayed balances currently match." }; } },
{ stage: "Split", action: "Run split",
text: "Apply the board-approved 2-for-1 split.",
summary: [["Operation", "2-for-1 split"], ["Multiplier", M("1.0 → 2.0 WAD")], ["Symbol", TOKEN], ["Network", NETWORK]],
run: (s) => { s.multiplier = 2; return { entries: [ok("MultiplierUpdated", "1.0 → 2.0 WAD"), nfo("scaledBalanceOf(Alice)", "200 EXM")], caption: "Displayed balances double while raw balances remain unchanged." }; } },
text: "Schedule the board-approved 2-for-1 split with updateUIMultiplier and let it take effect.",
summary: [["Operation", "updateUIMultiplier"], ["Multiplier", M("1.0 → 2.0 WAD")], ["Symbol", TOKEN], ["Network", NETWORK]],
run: (s) => { s.multiplier = 2; return { entries: [ok("UIMultiplierUpdated", "1.0 → 2.0 WAD"), nfo("balanceOfUI(Alice)", "200 EXM")], caption: "Displayed balances double at effectiveAt while raw balances remain unchanged." }; } },
],
},
pause: {
Expand Down Expand Up @@ -550,24 +550,28 @@ export const AssetDemo = ({ flow }) => {
return {
entries: [
txOk(engine, "B20Created", short(created.token), created),
nfo("multiplier()", `${state.multiplier}.0 WAD`, engine.explorerAddress(ctx.token)),
nfo("uiMultiplier()", `${state.multiplier}.0 WAD`, engine.explorerAddress(ctx.token)),
],
caption: "Raw and displayed balances currently match.",
};
},
async (engine, ctx, state) => {
const tx = await engine.updateMultiplier({ token: ctx.token, multiplier: 2n * 10n ** 18n });
// effectiveAt must be strictly in the future when the call lands. The margin covers a
// faucet top-up, the cross-tab send lock, or a slow RPC before inclusion.
const effectiveAt = (await engine.latestTimestamp()) + 15n;
const tx = await engine.updateUIMultiplier({ token: ctx.token, multiplier: 2n * 10n ** 18n, effectiveAt });
await engine.waitForTimestamp(effectiveAt);
const [multiplier, displayed] = await Promise.all([
engine.assetMultiplier(ctx.token),
engine.scaledBalanceOf(ctx.token, ctx.addresses.Alice),
engine.balanceOfUI(ctx.token, ctx.addresses.Alice),
]);
state.multiplier = Number(multiplier / 10n ** 18n);
return {
entries: [
txOk(engine, "MultiplierUpdated", "1.0 → 2.0 WAD", tx),
nfo("scaledBalanceOf(Alice)", `${engine.displayUnits(displayed)} EXM`, engine.explorerAddress(ctx.token)),
txOk(engine, "UIMultiplierUpdated", "1.0 → 2.0 WAD", tx),
nfo("balanceOfUI(Alice)", `${engine.displayUnits(displayed)} EXM`, engine.explorerAddress(ctx.token)),
],
caption: "Displayed balances doubled while the raw balances stayed unchanged.",
caption: "Displayed balances doubled at effectiveAt while the raw balances stayed unchanged.",
};
},
],
Expand Down Expand Up @@ -646,7 +650,7 @@ export const AssetDemo = ({ flow }) => {
const out = await LIVE_RUNNERS[active][stepIndex](engine, ctx, state);
if (ctx.token) {
try {
setAccountTokenBalance(engine.displayUnits(await engine.scaledBalanceOf(ctx.token, ctx.account)));
setAccountTokenBalance(engine.displayUnits(await engine.balanceOfUI(ctx.token, ctx.account)));
} catch {
setAccountTokenBalance(null);
}
Expand Down
12 changes: 6 additions & 6 deletions docs/snippets/B20PlaygroundDemo.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,10 @@ export const B20FlowDemo = ({ flow }) => {
s.nonces[owner] = current + 1;
return entries.push(emit("Approval", `${owner} → ${spender} · ${amt} (permit, relayed by ${me})`, 0));
}
case "updateMultiplier": {
case "updateUIMultiplier": {
if (!roleHas("OPERATOR_ROLE", me)) return entries.push(revert("AccessControlUnauthorizedAccount", `${me} lacks OPERATOR_ROLE`));
s.token = { ...s.token, multiplier: op.value };
return entries.push(emit("MultiplierUpdated", `${op.value}× (WAD)`, 0));
return entries.push(emit("UIMultiplierUpdated", `${op.value}× (WAD) · takes effect at effectiveAt`, 0));
}
case "announceBatchMint": {
const { recipients, amt, id } = op;
Expand Down Expand Up @@ -358,10 +358,10 @@ export const B20FlowDemo = ({ flow }) => {
stage: "Split",
action: "Run the split",
text: "The board declares a 2-for-1 split.",
summary: [["Operation", "updateMultiplier"], ["Role", "OPERATOR_ROLE"], ["Multiplier", M("2.0×")], ["Network", NETWORK]],
summary: [["Operation", "updateUIMultiplier"], ["Role", "OPERATOR_ROLE"], ["Multiplier", M("2.0×")], ["Effective", "scheduled effectiveAt"], ["Network", NETWORK]],
run: (s) => ({
entries: runOps(s, [{ as: "Issuer", type: "updateMultiplier", value: 2.0 }]),
caption: "Every balance doubles in one call, without a migration or a new contract.",
entries: runOps(s, [{ as: "Issuer", type: "updateUIMultiplier", value: 2.0 }]),
caption: "One call schedules the split. At effectiveAt every displayed balance doubles, without a migration or a new contract.",
}),
},
{
Expand Down Expand Up @@ -465,7 +465,7 @@ export const B20FlowDemo = ({ flow }) => {
<div style={{ display: "flex", alignItems: "baseline", gap: 8, marginBottom: 8 }}>
<div className="wf-t-caption" style={{ color: C.sub }}>{heading}</div>
{scaled && sim.token.multiplier !== 1 && (
<span style={{ fontFamily: mono, fontSize: 10.5, color: C.sub }}>multiplier() = {sim.token.multiplier}×</span>
<span style={{ fontFamily: mono, fontSize: 10.5, color: C.sub }}>uiMultiplier() = {sim.token.multiplier}×</span>
)}
</div>
<div style={{ display: "grid", gap: 6 }}>
Expand Down
2 changes: 1 addition & 1 deletion docs/snippets/StablecoinDemo.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const StablecoinDemo = ({ flow }) => {
const moduleUrl = (source) => URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
const [aaSource, engineSource] = await Promise.all([
fetchText("/static/aa.txt"),
fetchText("/static/vibenet-engine.txt?v=4"),
fetchText("/static/vibenet-engine.txt?v=5"),
]);
const aaUrl = moduleUrl(aaSource);
const rewritten = engineSource.replace('"./aa.txt"', JSON.stringify(aaUrl));
Expand Down
4 changes: 2 additions & 2 deletions docs/specifications/b20/reference/constants.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,5 @@ description: "B20 precompile addresses, role identifiers, policy scopes, and val

| Name | Value | Purpose |
|---|---|---|
| `WAD_PRECISION` | `1e18` | Fixed-point precision used to scale `multiplier`; `multiplier`, `toUIAmount`, and `fromUIAmount` all divide/multiply by this. |
| `MAX_UI_MULTIPLIER` | `type(uint128).max` | Maximum multiplier the setters accept — the overflow guard enforced by `updateMultiplier` and `updateUIMultiplier`. Exposed so callers can read the bound without triggering `InvalidMultiplier`. |
| `WAD_PRECISION` | `1e18` | Fixed-point precision used to scale `uiMultiplier`; `uiMultiplier`, `toUIAmount`, and `fromUIAmount` all divide/multiply by this. |
| `MAX_UI_MULTIPLIER` | `type(uint128).max` | Maximum multiplier the setters accept. It is the overflow guard enforced by `updateUIMultiplier` and the deprecated `updateMultiplier`. Exposed so callers can read the bound without triggering `InvalidMultiplier`. |
49 changes: 36 additions & 13 deletions docs/static/vibenet-engine.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from "./aa.txt";

export const CHAIN_ID = 84538453;
export const ENGINE_VERSION = 4;
export const ENGINE_VERSION = 5;
export const NETWORK_NAME = "Base Vibenet";
export const API_URL = "https://api.vibes.base.org";
export const ACCOUNT_RPC = `${API_URL}/api/vibenet/account/rpc`;
Expand Down Expand Up @@ -174,9 +174,11 @@ const assetAbi = [
{ type: "function", name: "isAnnouncementIdUsed", stateMutability: "view", inputs: [{ type: "string" }], outputs: [{ type: "bool" }] },
{ type: "function", name: "updateExtraMetadata", stateMutability: "nonpayable", inputs: [{ type: "string" }, { type: "string" }], outputs: [] },
{ type: "function", name: "extraMetadata", stateMutability: "view", inputs: [{ type: "string" }], outputs: [{ type: "string" }] },
{ type: "function", name: "updateMultiplier", stateMutability: "nonpayable", inputs: [{ type: "uint256" }], outputs: [] },
{ type: "function", name: "multiplier", stateMutability: "view", inputs: [], outputs: [{ type: "uint256" }] },
{ type: "function", name: "scaledBalanceOf", stateMutability: "view", inputs: [{ type: "address" }], outputs: [{ type: "uint256" }] },
{ type: "function", name: "updateUIMultiplier", stateMutability: "nonpayable", inputs: [{ type: "uint256" }, { type: "uint256" }], outputs: [] },
{ type: "function", name: "uiMultiplier", stateMutability: "view", inputs: [], outputs: [{ type: "uint256" }] },
{ type: "function", name: "newUIMultiplier", stateMutability: "view", inputs: [], outputs: [{ type: "uint256" }] },
{ type: "function", name: "effectiveAt", stateMutability: "view", inputs: [], outputs: [{ type: "uint256" }] },
{ type: "function", name: "balanceOfUI", stateMutability: "view", inputs: [{ type: "address" }], outputs: [{ type: "uint256" }] },
];

const client = createPublicClient({
Expand Down Expand Up @@ -668,17 +670,36 @@ export async function isAnnouncementIdUsed(token, id) {
return readFunction(token, assetAbi, "isAnnouncementIdUsed", [id]);
}

export async function updateMultiplier({ token, multiplier }) {
const data = encodeFunctionData({ abi: assetAbi, functionName: "updateMultiplier", args: [BigInt(multiplier)] });
return sendCalls({ calls: [{ to: token, data }], metadata: "Update asset multiplier" });
export async function latestTimestamp() {
const block = await rpc("eth_getBlockByNumber", ["latest", false]);
return BigInt(block.timestamp);
}

// Resolves once the chain head reaches `timestamp`, so a scheduled
// multiplier has matured before the demo reads it back.
export async function waitForTimestamp(timestamp, { timeoutMs = 60_000 } = {}) {
const deadline = Date.now() + timeoutMs;
while ((await latestTimestamp()) < BigInt(timestamp)) {
if (Date.now() > deadline) throw new Error("Timed out waiting for the scheduled multiplier to take effect");
await new Promise((resolve) => setTimeout(resolve, 250));
}
}

export async function updateUIMultiplier({ token, multiplier, effectiveAt }) {
const data = encodeFunctionData({
abi: assetAbi,
functionName: "updateUIMultiplier",
args: [BigInt(multiplier), BigInt(effectiveAt)],
});
return sendCalls({ calls: [{ to: token, data }], metadata: "Schedule asset multiplier update" });
}

export async function assetMultiplier(token) {
return readFunction(token, assetAbi, "multiplier");
return readFunction(token, assetAbi, "uiMultiplier");
}

export async function scaledBalanceOf(token, address) {
return readFunction(token, assetAbi, "scaledBalanceOf", [address]);
export async function balanceOfUI(token, address) {
return readFunction(token, assetAbi, "balanceOfUI", [address]);
}

export async function assetMetadata(token, key) {
Expand All @@ -691,7 +712,7 @@ export async function assetDetails(token) {
readFunction(token, b20Abi, "decimals"),
readFunction(token, b20Abi, "totalSupply"),
readFunction(token, b20Abi, "supplyCap"),
readFunction(token, assetAbi, "multiplier"),
readFunction(token, assetAbi, "uiMultiplier"),
]);
return { name, decimals: Number(decimals), totalSupply, supplyCap, multiplier };
}
Expand Down Expand Up @@ -864,9 +885,11 @@ const api = {
batchMint,
announceDistribution,
isAnnouncementIdUsed,
updateMultiplier,
latestTimestamp,
waitForTimestamp,
updateUIMultiplier,
assetMultiplier,
scaledBalanceOf,
balanceOfUI,
assetMetadata,
assetDetails,
createPolicy,
Expand Down
4 changes: 4 additions & 0 deletions docs/upgrades/beryl/b20.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@

### Creating and Managing Policies

```solidity Policy Management Example lines wrap expandable

Check warning on line 97 in docs/upgrades/beryl/b20.mdx

View workflow job for this annotation

GitHub Actions / Docs Style / Conformance

Docs style

[codeblock/highlight] Consider `highlight={}` to draw attention to key lines
// Create a policy (admin first, then type)
uint64 policyId = policyRegistry.createPolicy(adminAddress, PolicyType.BLOCKLIST);
// Or seed the initial member set in one call:
Expand Down Expand Up @@ -232,6 +232,10 @@
| `toRawBalance(scaled)` | Convert scaled amount to raw |
| `updateMultiplier(newMultiplier)` | Update the multiplier. Gated by `OPERATOR_ROLE`. |

<Note>
These are the Beryl names. Cobalt adds the ERC-8056 interface: `uiMultiplier` and `balanceOfUI` alias `multiplier` and `scaledBalanceOf`, which stay canonical; `toUIAmount` and `fromUIAmount` replace the deprecated `toScaledBalance` and `toRawBalance`; and the scheduled `updateUIMultiplier` setter replaces `updateMultiplier`, which stays callable as a deprecated emergency setter. The scheduled setter emits only `UIMultiplierUpdated`; the legacy `MultiplierUpdated` event is emitted only by `updateMultiplier`. See the [Cobalt multiplier changelog](/base-chain/specs/reference/b20/changelog/02-cobalt-b20asset-multiplier#functions).
</Note>

#### Announcements

On-chain disclosure brackets that wrap sensitive operations (e.g. batch mints, multiplier updates) with a public notice period. Gated by `OPERATOR_ROLE`.
Expand Down
3 changes: 0 additions & 3 deletions examples/verified-doc-samples/typescript/src/b20/abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,11 @@ const assetExtraAbi = parseAbi([
"function batchMint(address[],uint256[])",
"function announce(bytes[],string,string,string)",
"function isAnnouncementIdUsed(string) view returns (bool)",
"function updateMultiplier(uint256)",
"function updateUIMultiplier(uint256,uint256)",
"function cancelUIMultiplierUpdate()",
"function multiplier() view returns (uint256)",
"function uiMultiplier() view returns (uint256)",
"function newUIMultiplier() view returns (uint256)",
"function effectiveAt() view returns (uint256)",
"function scaledBalanceOf(address) view returns (uint256)",
"function balanceOfUI(address) view returns (uint256)",
]);

Expand Down
Loading
Loading