From 44c5ed300aedd165dd59223235b09c0d93c796fa Mon Sep 17 00:00:00 2001 From: Soheima M Date: Wed, 17 Jun 2026 20:34:39 +0100 Subject: [PATCH 1/8] added integration guide --- docs/apps/guides/accept-b20-payments.mdx | 60 +++++++ docs/docs.json | 4 +- docs/get-started/launch-b20-token.md | 207 +++++++++++++++++++++++ 3 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 docs/apps/guides/accept-b20-payments.mdx create mode 100644 docs/get-started/launch-b20-token.md diff --git a/docs/apps/guides/accept-b20-payments.mdx b/docs/apps/guides/accept-b20-payments.mdx new file mode 100644 index 000000000..51635e716 --- /dev/null +++ b/docs/apps/guides/accept-b20-payments.mdx @@ -0,0 +1,60 @@ +--- +title: "Accept B20 payments in an app" +description: "Take B20 token payments in your app and match each one to an order with on-chain memos." +--- + +B20 is an ERC-20 superset. Standard `transfer`, `transferFrom`, `approve`, `balanceOf`, and ERC-2612 `permit` all work, so an app that accepts ERC-20 tokens accepts B20 with no code changes. + +B20's additions include transfer policies, pausing, supply caps, and memos. This guide uses the memo: `transferWithMemo` works like `transfer`, but also attaches a `bytes32` reference such as an order ID and emits a `Memo` event right after the standard `Transfer`. Read that event to tie each payment to an order. + +## Tag a payment with a memo + +This uses your configured viem `walletClient` and `publicClient`. It reads the token's decimals, sends a payment tagged with an order ID, then reads the memo back from the receipt: + +```js theme={null} +import { parseUnits, stringToHex, hexToString, parseEventLogs } from 'viem'; + +const TOKEN = '0xB20f...'; // the B20 token you accept +const MERCHANT = '0x...'; // where payments land + +const ABI = [ + { type: 'function', name: 'decimals', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint8' }] }, + { type: 'function', name: 'transferWithMemo', stateMutability: 'nonpayable', + inputs: [{ name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }, { name: 'memo', type: 'bytes32' }], + outputs: [{ type: 'bool' }] }, + { type: 'event', name: 'Memo', inputs: [ + { name: 'caller', type: 'address', indexed: true }, + { name: 'memo', type: 'bytes32', indexed: true }, + ] }, +]; + +// Read decimals because B20 tokens range from 6 to 18. +const decimals = await publicClient.readContract({ address: TOKEN, abi: ABI, functionName: 'decimals' }); + +// Pay 10 tokens, tagging the transfer with an order ID. +const hash = await walletClient.writeContract({ + address: TOKEN, abi: ABI, functionName: 'transferWithMemo', + args: [MERCHANT, parseUnits('10', decimals), stringToHex('order-42', { size: 32 })], +}); + +// The Memo event carries the order ID back. Read it from the receipt. +const receipt = await publicClient.waitForTransactionReceipt({ hash }); +const [memo] = parseEventLogs({ abi: ABI, logs: receipt.logs, eventName: 'Memo' }); +console.log(hexToString(memo.args.memo, { size: 32 }).replace(/\0+$/, '')); // "order-42" +``` + +To collect with an allowance instead of a direct transfer, use `transferFromWithMemo`. It emits the same `Memo` event. + +## Handle B20-specific reverts + +A B20 transfer can revert where a plain ERC-20 would not. Surface these so a failed payment is visible, not silent: + +- `PolicyForbids`: the sender or recipient is not authorized by the token's transfer policy. Most tokens are open by default, but a regulated issuer can gate transfers with an allowlist or blocklist. +- A paused transfer: the issuer paused the token's `TRANSFER` feature. + +Call `publicClient.simulateContract` with the same arguments before sending. It raises these as typed errors before the user signs, so you can show the reason instead of a failed transaction. + +## Related pages + +- [B20 token standard](/base-chain/specs/upgrades/beryl/b20): the full interface, including memos, policies, pausing, and roles. +- [Launch a B20 Token](/get-started/launch-b20-token): create your own B20 token. diff --git a/docs/docs.json b/docs/docs.json index f5d56f00d..e9c381ef3 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -40,6 +40,7 @@ "get-started/resources-for-ai-agents", "get-started/build-app", "get-started/launch-token", + "get-started/launch-b20-token", "get-started/deploy-smart-contracts", "get-started/learning-resources" ] @@ -665,7 +666,8 @@ "group": "Guides", "pages": [ "apps/technical-guides/base-notifications", - "apps/guides/migrate-to-standard-web-app" + "apps/guides/migrate-to-standard-web-app", + "apps/guides/accept-b20-payments" ] }, { diff --git a/docs/get-started/launch-b20-token.md b/docs/get-started/launch-b20-token.md new file mode 100644 index 000000000..267a42d33 --- /dev/null +++ b/docs/get-started/launch-b20-token.md @@ -0,0 +1,207 @@ +--- +title: "Launch a B20 Token" +description: "Spin up a fully configured ERC-20 superset token on Base through the singleton B20 Factory precompile. Set the admin, minter, and supply cap, mint your initial supply, and confirm it on-chain, all without writing or deploying any Solidity." +--- + +B20 is an ERC-20 superset that runs as a native precompile on Base. You don't write or deploy a token contract. Instead, you ask the singleton **B20 Factory** to create one, fully configured, in a single transaction. + +By the end you have a live B20 Asset token (with an admin, a minter, and a supply cap) holding minted supply you can view in the explorer. To accept it as payment in an app, continue with [Accept B20 payments](/apps/guides/accept-b20-payments). + +## Before you begin + +You need: + +* [Foundry](https://getfoundry.sh) installed. You'll use `cast` from it. +* **Base's `forge` build** for deploying. A B20 token is a native precompile, so the factory address holds no contract bytecode. Stock `forge` can't simulate a call to it and aborts a deploy script with `call to non-contract address`. Base's [`base-anvil`](https://github.com/base/base-anvil) fork of `forge` registers the precompiles into its EVM. Build it once and put it ahead of stock `forge` on your `PATH`: + + ```bash theme={null} + git clone https://github.com/base/base-anvil && cd base-anvil + cargo build --release -p forge # produces target/release/forge + ``` +* The network details and a funded account. This tutorial targets **Base Vibenet**, the testnet that hosts the B20 precompiles: + + | Setting | Value | + |---|---| + | Network | Base Vibenet | + | RPC URL | `https://rpc.vibes.base.org/` | + | Chain ID | `84538453` | + | Faucet | `https://faucet.vibes.base.org/` | + | Explorer | _(view the faucet site → Explorer)_ | + +Create a `.env` in your project (and add `.env` to `.gitignore`): + +```bash .env theme={null} +RPC_URL="https://rpc.vibes.base.org/" +# One account plays deployer + admin + minter for this quickstart. +PRIVATE_KEY="0x..." +ACCOUNT_ADDRESS="0x..." +``` + + +If you don't have an account, `cast wallet new` prints a fresh address and key. Put the key in `PRIVATE_KEY` and the address in `ACCOUNT_ADDRESS`. + + +Request testnet ETH for `ACCOUNT_ADDRESS` from the faucet above, then confirm it arrived on Vibenet (not another chain: the address is the same everywhere, but balances are per chain): + +```bash theme={null} +source .env +cast balance $ACCOUNT_ADDRESS --rpc-url $RPC_URL +``` + + +The command prints a non-zero balance. You're funding one account here. It signs the deploy and the mint, and receives the minted supply. Production setups can separate the admin, minter, and recipient, but this guide doesn't require that split. + + +## Set up your project + +```bash theme={null} +forge init b20-quickstart && cd b20-quickstart +forge install base/base-std +``` + +Add the remappings and the `base = true` flag to `foundry.toml` (under `[profile.default]`). `base = true` tells Base's `forge` build to run the B20 precompiles inside its EVM, so the deploy script's local simulation can call the factory: + +```toml foundry.toml theme={null} +base = true +remappings = [ + "base-std/=lib/base-std/src/", + "base-std-test/=lib/base-std/test/", +] +``` + + +The interfaces compile with any Solidity `>=0.8.20 <0.9.0`. + + +## Create your token + +The factory's single entry point is `createB20(variant, salt, params, initCalls)`: + +* `variant`: `ASSET` or `STABLECOIN`. This guide uses `ASSET`. +* `salt`: caller-chosen entropy that fixes the deterministic token address. +* `params`: ABI-encoded name, symbol, initial admin, and decimals. +* `initCalls`: optional batch of config calls applied at creation. + + + + Use `B20FactoryLib` to encode `params` and `initCalls`. Create `script/CreateToken.s.sol`: + + ```solidity script/CreateToken.s.sol theme={null} + // SPDX-License-Identifier: MIT + pragma solidity ^0.8.20; + + import {Script, console} from "forge-std/Script.sol"; + + import {B20Constants} from "base-std/lib/B20Constants.sol"; + import {B20FactoryLib} from "base-std/lib/B20FactoryLib.sol"; + import {IB20Factory} from "base-std/interfaces/IB20Factory.sol"; + import {StdPrecompiles} from "base-std/StdPrecompiles.sol"; + + contract CreateToken is Script { + function run() external returns (address token) { + // For the quickstart, one account is admin + minter. + address account = vm.envAddress("ACCOUNT_ADDRESS"); + bytes32 salt = keccak256("my-first-b20"); + + // Name, symbol, initial DEFAULT_ADMIN_ROLE holder, decimals (6-18). + bytes memory params = B20FactoryLib.encodeAssetCreateParams("My Token", "MYT", account, 18); + + // Configuration applied atomically at creation. + bytes[] memory initCalls = new bytes[](2); + initCalls[0] = B20FactoryLib.encodeGrantRole(B20Constants.MINT_ROLE, account); + initCalls[1] = B20FactoryLib.encodeUpdateSupplyCap(1_000_000e18); + + vm.startBroadcast(); + token = StdPrecompiles.B20_FACTORY.createB20(IB20Factory.B20Variant.ASSET, salt, params, initCalls); + vm.stopBroadcast(); + + console.log("B20 token created at:", token); + } + } + ``` + + + **Encode with `B20FactoryLib`.** The native implementation rejects non-canonical calldata with `AbiDecodeFailed`; the helpers produce canonical encoding. + + + + Asset decimals are fixed at creation and must be in `[6, 18]`. The supply cap is optional; the no-cap sentinel is `type(uint128).max` (the cap can never exceed `uint128.max`). + + + + + ```bash theme={null} + source .env + forge script script/CreateToken.s.sol --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast + ``` + + On success the script logs the new token's address. Note the `0xB200...` prefix: the factory itself is `0xB20f...`, and the tokens it creates start `0xB200...`: + + ```text theme={null} + == Logs == + B20 token created at: 0xB200... + ``` + + + + Save it into your environment so the next step needs no copy-paste. The broadcast artifact holds the return value: + + ```bash theme={null} + export TOKEN_ADDRESS=$(jq -r '.returns.token.value' \ + broadcast/CreateToken.s.sol/84538453/run-latest.json) + echo "TOKEN_ADDRESS=$TOKEN_ADDRESS" + ``` + + + +## Mint and verify + +Minting requires `MINT_ROLE`, which `initCalls` granted to your account. + + + + ```bash theme={null} + cast send $TOKEN_ADDRESS "mint(address,uint256)" $ACCOUNT_ADDRESS 1000e18 \ + --rpc-url $RPC_URL --private-key $PRIVATE_KEY + ``` + + `cast send` prints a receipt with `status 1 (success)`. + + + + ```bash theme={null} + cast call $TOKEN_ADDRESS "balanceOf(address)(uint256)" $ACCOUNT_ADDRESS --rpc-url $RPC_URL + # 1000000000000000000000 [1e21] + ``` + + + The token now holds minted supply on-chain. Open the explorer (from the faucet site) and search `$TOKEN_ADDRESS` to view it. + + + + +## What you built + +In this guide you: + +* created a B20 Asset token with one `createB20` call, +* configured its admin, minter, and supply cap atomically via `initCalls`, +* minted supply, and +* verified the balance on-chain. + +You did all of this without writing, deploying, or auditing a token contract. + +## Next steps + + +Wire this token into a checkout flow that tags each payment with an order ID and reconciles it from on-chain events. + + +* Gate transfers or mints with PolicyRegistry policies, add granular pause, or manage roles. See the B20 overview. +* Issue a stablecoin variant (fixed 6 decimals, immutable currency code). + + + +With a standard ERC-20 you write (or fork) a token contract, audit it, deploy it, and add access control, pausing, and compliance hooks yourself. With B20, that logic is part of the chain. Every token the factory creates runs the same native implementation, with roles, supply cap, pause, policy gating, memos, and `permit` built in at full ERC-20 selector parity. Tooling that targets the ERC-20 interface works with it unchanged. + + From 6c69c464cb79e8865f12fa78a5caae0d8c50b40d Mon Sep 17 00:00:00 2001 From: Soheima M Date: Wed, 17 Jun 2026 20:54:21 +0100 Subject: [PATCH 2/8] added updates on b20 --- docs/get-started/launch-b20-token.md | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/docs/get-started/launch-b20-token.md b/docs/get-started/launch-b20-token.md index 267a42d33..e20167bef 100644 --- a/docs/get-started/launch-b20-token.md +++ b/docs/get-started/launch-b20-token.md @@ -1,18 +1,20 @@ --- title: "Launch a B20 Token" -description: "Spin up a fully configured ERC-20 superset token on Base through the singleton B20 Factory precompile. Set the admin, minter, and supply cap, mint your initial supply, and confirm it on-chain, all without writing or deploying any Solidity." +description: "Launch a B20 token on Base by calling the B20 Factory precompile — no Solidity to write or deploy." --- -B20 is an ERC-20 superset that runs as a native precompile on Base. You don't write or deploy a token contract. Instead, you ask the singleton **B20 Factory** to create one, fully configured, in a single transaction. +B20 is an ERC-20 superset that runs as a native precompile on Base. Roles, supply caps, pausing, policy gating, memos, and `permit` are built into the chain, at full ERC-20 selector parity. -By the end you have a live B20 Asset token (with an admin, a minter, and a supply cap) holding minted supply you can view in the explorer. To accept it as payment in an app, continue with [Accept B20 payments](/apps/guides/accept-b20-payments). +A standard ERC-20 leaves that logic for you to build, audit, and maintain. With B20, you call the singleton **B20 Factory** to create a token, fully configured, in a single transaction. + +This guide creates an Asset token, mints its initial supply, and verifies the balance on-chain. To accept the token as payment in an app, continue with [Accept B20 payments](/apps/guides/accept-b20-payments). ## Before you begin You need: * [Foundry](https://getfoundry.sh) installed. You'll use `cast` from it. -* **Base's `forge` build** for deploying. A B20 token is a native precompile, so the factory address holds no contract bytecode. Stock `forge` can't simulate a call to it and aborts a deploy script with `call to non-contract address`. Base's [`base-anvil`](https://github.com/base/base-anvil) fork of `forge` registers the precompiles into its EVM. Build it once and put it ahead of stock `forge` on your `PATH`: +* **Base's `forge` build** for deploying. A B20 token is a native precompile, so the factory address holds no contract bytecode. Standard `forge` cannot simulate a call to that address. It aborts the deploy script with `call to non-contract address`. Base's [`base-anvil`](https://github.com/base/base-anvil) fork of `forge` registers the precompiles into its EVM. Build it once and put it ahead of standard `forge` on your `PATH`: ```bash theme={null} git clone https://github.com/base/base-anvil && cd base-anvil @@ -135,7 +137,7 @@ The factory's single entry point is `createB20(variant, salt, params, initCalls) forge script script/CreateToken.s.sol --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast ``` - On success the script logs the new token's address. Note the `0xB200...` prefix: the factory itself is `0xB20f...`, and the tokens it creates start `0xB200...`: + On success the script logs the new token's address. The factory's address starts `0xB20f...`. The tokens it creates start `0xB200...`: ```text theme={null} == Logs == @@ -144,7 +146,7 @@ The factory's single entry point is `createB20(variant, salt, params, initCalls) - Save it into your environment so the next step needs no copy-paste. The broadcast artifact holds the return value: + Save the address to an environment variable so the next step needs no copy-paste. The broadcast artifact holds the return value: ```bash theme={null} export TOKEN_ADDRESS=$(jq -r '.returns.token.value' \ @@ -193,15 +195,6 @@ You did all of this without writing, deploying, or auditing a token contract. ## Next steps - -Wire this token into a checkout flow that tags each payment with an order ID and reconciles it from on-chain events. - - -* Gate transfers or mints with PolicyRegistry policies, add granular pause, or manage roles. See the B20 overview. +* [Accept B20 payments in an app](/apps/guides/accept-b20-payments): wire this token into a checkout flow that tags each payment with an order ID and reconciles it from on-chain events. +* Gate transfers or mints with PolicyRegistry policies, add granular pause, or manage roles. See the [B20 token standard](/base-chain/specs/upgrades/beryl/b20). * Issue a stablecoin variant (fixed 6 decimals, immutable currency code). - - - -With a standard ERC-20 you write (or fork) a token contract, audit it, deploy it, and add access control, pausing, and compliance hooks yourself. With B20, that logic is part of the chain. Every token the factory creates runs the same native implementation, with roles, supply cap, pause, policy gating, memos, and `permit` built in at full ERC-20 selector parity. Tooling that targets the ERC-20 interface works with it unchanged. - - From 41287017e64b82c5d524271dce1179bc0bd9c322 Mon Sep 17 00:00:00 2001 From: roethke Date: Wed, 17 Jun 2026 17:13:32 -0700 Subject: [PATCH 3/8] add alternative deployment environments --- docs/get-started/launch-b20-token.md | 178 +++++++++++++++++++-------- 1 file changed, 126 insertions(+), 52 deletions(-) diff --git a/docs/get-started/launch-b20-token.md b/docs/get-started/launch-b20-token.md index e20167bef..67003f468 100644 --- a/docs/get-started/launch-b20-token.md +++ b/docs/get-started/launch-b20-token.md @@ -11,54 +11,22 @@ This guide creates an Asset token, mints its initial supply, and verifies the ba ## Before you begin -You need: - -* [Foundry](https://getfoundry.sh) installed. You'll use `cast` from it. -* **Base's `forge` build** for deploying. A B20 token is a native precompile, so the factory address holds no contract bytecode. Standard `forge` cannot simulate a call to that address. It aborts the deploy script with `call to non-contract address`. Base's [`base-anvil`](https://github.com/base/base-anvil) fork of `forge` registers the precompiles into its EVM. Build it once and put it ahead of standard `forge` on your `PATH`: +You need **Base's Foundry build** (`base-forge`, `base-cast`, `base-anvil`). Install it via `base-foundryup`: ```bash theme={null} - git clone https://github.com/base/base-anvil && cd base-anvil - cargo build --release -p forge # produces target/release/forge + curl -L https://raw.githubusercontent.com/base/base-anvil/HEAD/foundryup/install | bash + base-foundryup --install v1.1.0 ``` -* The network details and a funded account. This tutorial targets **Base Vibenet**, the testnet that hosts the B20 precompiles: - - | Setting | Value | - |---|---| - | Network | Base Vibenet | - | RPC URL | `https://rpc.vibes.base.org/` | - | Chain ID | `84538453` | - | Faucet | `https://faucet.vibes.base.org/` | - | Explorer | _(view the faucet site → Explorer)_ | - -Create a `.env` in your project (and add `.env` to `.gitignore`): - -```bash .env theme={null} -RPC_URL="https://rpc.vibes.base.org/" -# One account plays deployer + admin + minter for this quickstart. -PRIVATE_KEY="0x..." -ACCOUNT_ADDRESS="0x..." -``` - -If you don't have an account, `cast wallet new` prints a fresh address and key. Put the key in `PRIVATE_KEY` and the address in `ACCOUNT_ADDRESS`. - - -Request testnet ETH for `ACCOUNT_ADDRESS` from the faucet above, then confirm it arrived on Vibenet (not another chain: the address is the same everywhere, but balances are per chain): - -```bash theme={null} -source .env -cast balance $ACCOUNT_ADDRESS --rpc-url $RPC_URL -``` - - -The command prints a non-zero balance. You're funding one account here. It signs the deploy and the mint, and receives the minted supply. Production setups can separate the admin, minter, and recipient, but this guide doesn't require that split. - + + Standard `forge` cannot simulate calls to B20 precompile addresses (they hold no contract bytecode) and aborts with `call to non-contract address`. Base's `base-forge` registers the precompiles into its EVM. It installs alongside your existing Foundry toolchain without overwriting it — use `base-forge`, `base-cast`, and `base-anvil` for all commands in this guide. + ## Set up your project ```bash theme={null} -forge init b20-quickstart && cd b20-quickstart -forge install base/base-std +base-forge init b20-quickstart && cd b20-quickstart +base-forge install base/base-std --no-git ``` Add the remappings and the `base = true` flag to `foundry.toml` (under `[profile.default]`). `base = true` tells Base's `forge` build to run the B20 precompiles inside its EVM, so the deploy script's local simulation can call the factory: @@ -75,6 +43,100 @@ remappings = [ The interfaces compile with any Solidity `>=0.8.20 <0.9.0`. +## Choose a network + +Pick a network with the B20 precompiles active, then create a `.env` **inside your `b20-quickstart` project directory**: + + + + | Setting | Value | + |---|---| + | RPC URL | `https://sepolia.base.org` | + | Chain ID | `84532` | + | Faucet | [CDP Faucet](https://portal.cdp.coinbase.com/products/faucet) or [other providers](/base-chain/network-information/network-faucets) | + | Explorer | [sepolia.basescan.org](https://sepolia.basescan.org) | + + ```bash .env theme={null} + export RPC_URL="https://sepolia.base.org" + export PRIVATE_KEY="0x..." + export ACCOUNT_ADDRESS="0x..." + export CHAIN_ID="84532" + ``` + + + If you don't have an account, `base-cast wallet new` prints a fresh address and key. + + + Request testnet ETH from the faucet, then confirm it arrived: + + ```bash theme={null} + source .env + base-cast balance $ACCOUNT_ADDRESS --rpc-url $RPC_URL + ``` + + + The command prints a non-zero balance. This account signs the deploy and the mint, and receives the minted supply. + + + + | Setting | Value | + |---|---| + | RPC URL | `https://rpc.vibes.base.org/` | + | Chain ID | `84538453` | + | Faucet | [faucet.vibes.base.org](https://faucet.vibes.base.org/) | + | Explorer | [explorer.vibes.base.org](https://explorer.vibes.base.org/) | + + ```bash .env theme={null} + export RPC_URL="https://rpc.vibes.base.org/" + export PRIVATE_KEY="0x..." + export ACCOUNT_ADDRESS="0x..." + export CHAIN_ID="84538453" + ``` + + + If you don't have an account, `base-cast wallet new` prints a fresh address and key. + + + Request testnet ETH from the faucet, then confirm it arrived: + + ```bash theme={null} + source .env + base-cast balance $ACCOUNT_ADDRESS --rpc-url $RPC_URL + ``` + + + The command prints a non-zero balance. This account signs the deploy and the mint, and receives the minted supply. + + + + Start a local Base node in a separate terminal: + + ```bash theme={null} + base-anvil + ``` + + Create a `.env` using anvil's pre-funded account #0: + + ```bash .env theme={null} + export RPC_URL="http://127.0.0.1:8545" + export PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + export ACCOUNT_ADDRESS="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + export CHAIN_ID="31337" + ``` + + Confirm the node is running: + + ```bash theme={null} + source .env + base-cast balance $ACCOUNT_ADDRESS --rpc-url $RPC_URL + ``` + + + The command prints `10000000000000000000000` — anvil's default pre-funded balance of 10,000 ETH. + + + + ## Create your token The factory's single entry point is `createB20(variant, salt, params, initCalls)`: @@ -134,11 +196,15 @@ The factory's single entry point is `createB20(variant, salt, params, initCalls) ```bash theme={null} source .env - forge script script/CreateToken.s.sol --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast + base-forge script script/CreateToken.s.sol --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast ``` On success the script logs the new token's address. The factory's address starts `0xB20f...`. The tokens it creates start `0xB200...`: + + If you see `TokenAlreadyExists`, the salt `keccak256("my-first-b20")` is already registered on this network or anvil instance. Either restart `base-anvil` for a fresh state, or change the salt in the script to a unique value. + + ```text theme={null} == Logs == B20 token created at: 0xB200... @@ -149,10 +215,18 @@ The factory's single entry point is `createB20(variant, salt, params, initCalls) Save the address to an environment variable so the next step needs no copy-paste. The broadcast artifact holds the return value: ```bash theme={null} - export TOKEN_ADDRESS=$(jq -r '.returns.token.value' \ - broadcast/CreateToken.s.sol/84538453/run-latest.json) - echo "TOKEN_ADDRESS=$TOKEN_ADDRESS" + TOKEN_ADDRESS=$(jq -er '.returns.token.value' \ + broadcast/CreateToken.s.sol/$CHAIN_ID/run-latest.json) \ + && echo "export TOKEN_ADDRESS=$TOKEN_ADDRESS" >> .env \ + && source .env \ + && echo "TOKEN_ADDRESS=$TOKEN_ADDRESS" ``` + + Appending to `.env` keeps `TOKEN_ADDRESS` available in later steps, even in a new terminal session. + + + The broadcast path includes the chain ID, which the `CHAIN_ID` value in your `.env` supplies: `84532` for Sepolia, `84538453` for Vibenet, `31337` for local base-anvil. + @@ -163,21 +237,21 @@ Minting requires `MINT_ROLE`, which `initCalls` granted to your account. ```bash theme={null} - cast send $TOKEN_ADDRESS "mint(address,uint256)" $ACCOUNT_ADDRESS 1000e18 \ + base-cast send $TOKEN_ADDRESS "mint(address,uint256)" $ACCOUNT_ADDRESS 1000000000000000000000 \ --rpc-url $RPC_URL --private-key $PRIVATE_KEY ``` - `cast send` prints a receipt with `status 1 (success)`. + `base-cast send` prints a receipt with `status 1 (success)`. ```bash theme={null} - cast call $TOKEN_ADDRESS "balanceOf(address)(uint256)" $ACCOUNT_ADDRESS --rpc-url $RPC_URL + base-cast call $TOKEN_ADDRESS "balanceOf(address)(uint256)" $ACCOUNT_ADDRESS --rpc-url $RPC_URL # 1000000000000000000000 [1e21] ``` - The token now holds minted supply on-chain. Open the explorer (from the faucet site) and search `$TOKEN_ADDRESS` to view it. + The token now holds minted supply on-chain. Search `$TOKEN_ADDRESS` in the explorer to view it. @@ -186,10 +260,10 @@ Minting requires `MINT_ROLE`, which `initCalls` granted to your account. In this guide you: -* created a B20 Asset token with one `createB20` call, -* configured its admin, minter, and supply cap atomically via `initCalls`, -* minted supply, and -* verified the balance on-chain. +* Created a B20 Asset token with one `createB20` call +* Configured its admin, minter, and supply cap atomically via `initCalls` +* Minted supply +* Verified the balance on-chain You did all of this without writing, deploying, or auditing a token contract. From 4be7e061aa02cf127c4d602f3aa5d20f3c0d37e7 Mon Sep 17 00:00:00 2001 From: roethke Date: Wed, 17 Jun 2026 17:18:27 -0700 Subject: [PATCH 4/8] fix ext --- docs/get-started/{launch-b20-token.md => launch-b20-token.mdx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/get-started/{launch-b20-token.md => launch-b20-token.mdx} (100%) diff --git a/docs/get-started/launch-b20-token.md b/docs/get-started/launch-b20-token.mdx similarity index 100% rename from docs/get-started/launch-b20-token.md rename to docs/get-started/launch-b20-token.mdx From 9cf00b3bc657cdce9c4993c7f0f4735862f334c2 Mon Sep 17 00:00:00 2001 From: roethke Date: Thu, 18 Jun 2026 09:28:02 -0700 Subject: [PATCH 5/8] add b20 token to card list, bump token sidebar order --- docs/docs.json | 2 +- docs/get-started/base.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs.json b/docs/docs.json index e9c381ef3..e7ad682de 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -39,8 +39,8 @@ "pages": [ "get-started/resources-for-ai-agents", "get-started/build-app", - "get-started/launch-token", "get-started/launch-b20-token", + "get-started/launch-token", "get-started/deploy-smart-contracts", "get-started/learning-resources" ] diff --git a/docs/get-started/base.mdx b/docs/get-started/base.mdx index d672db9a9..031656d4f 100644 --- a/docs/get-started/base.mdx +++ b/docs/get-started/base.mdx @@ -19,9 +19,9 @@ mode: "wide"
### Tokens + [Launch a B20 Token](/get-started/launch-b20-token) [Launch a Token](/get-started/launch-token) [Bridge from Solana](/base-chain/quickstart/base-solana-bridge) - [Bridge from Ethereum](/base-chain/network-information/bridges)
From 5de08e395b9339070fb295468d6a545d49307772 Mon Sep 17 00:00:00 2001 From: roethke Date: Thu, 18 Jun 2026 09:48:20 -0700 Subject: [PATCH 6/8] add precompile benefits, terminal titles, stablecoin variant --- docs/get-started/launch-b20-token.mdx | 48 +++++++++++++++++---------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/docs/get-started/launch-b20-token.mdx b/docs/get-started/launch-b20-token.mdx index 67003f468..bc8389f9f 100644 --- a/docs/get-started/launch-b20-token.mdx +++ b/docs/get-started/launch-b20-token.mdx @@ -1,19 +1,19 @@ --- title: "Launch a B20 Token" -description: "Launch a B20 token on Base by calling the B20 Factory precompile — no Solidity to write or deploy." +description: "Launch a B20 token on Base by calling the B20 Factory precompile." --- -B20 is an ERC-20 superset that runs as a native precompile on Base. Roles, supply caps, pausing, policy gating, memos, and `permit` are built into the chain, at full ERC-20 selector parity. +B20 is an ERC-20 superset that runs as a native precompile on Base, which makes transfers cheaper and higher-throughput than a standard contract token while keeping full ERC-20 compatibility. Roles, supply caps, pausing, policy gating, memos, and `permit` are built into the chain. A standard ERC-20 leaves that logic for you to build, audit, and maintain. With B20, you call the singleton **B20 Factory** to create a token, fully configured, in a single transaction. -This guide creates an Asset token, mints its initial supply, and verifies the balance on-chain. To accept the token as payment in an app, continue with [Accept B20 payments](/apps/guides/accept-b20-payments). +This guide creates an Asset token, mints its initial supply, and verifies the balance onchain. To accept the token as payment in an app, continue with [Accept B20 payments](/apps/guides/accept-b20-payments). ## Before you begin You need **Base's Foundry build** (`base-forge`, `base-cast`, `base-anvil`). Install it via `base-foundryup`: - ```bash theme={null} + ```bash Terminal theme={null} curl -L https://raw.githubusercontent.com/base/base-anvil/HEAD/foundryup/install | bash base-foundryup --install v1.1.0 ``` @@ -24,7 +24,7 @@ You need **Base's Foundry build** (`base-forge`, `base-cast`, `base-anvil`). Ins ## Set up your project -```bash theme={null} +```bash Terminal theme={null} base-forge init b20-quickstart && cd b20-quickstart base-forge install base/base-std --no-git ``` @@ -69,7 +69,7 @@ Pick a network with the B20 precompiles active, then create a `.env` **inside yo Request testnet ETH from the faucet, then confirm it arrived: - ```bash theme={null} + ```bash Terminal theme={null} source .env base-cast balance $ACCOUNT_ADDRESS --rpc-url $RPC_URL ``` @@ -99,7 +99,7 @@ Pick a network with the B20 precompiles active, then create a `.env` **inside yo Request testnet ETH from the faucet, then confirm it arrived: - ```bash theme={null} + ```bash Terminal theme={null} source .env base-cast balance $ACCOUNT_ADDRESS --rpc-url $RPC_URL ``` @@ -111,7 +111,7 @@ Pick a network with the B20 precompiles active, then create a `.env` **inside yo Start a local Base node in a separate terminal: - ```bash theme={null} + ```bash Terminal theme={null} base-anvil ``` @@ -126,7 +126,7 @@ Pick a network with the B20 precompiles active, then create a `.env` **inside yo Confirm the node is running: - ```bash theme={null} + ```bash Terminal theme={null} source .env base-cast balance $ACCOUNT_ADDRESS --rpc-url $RPC_URL ``` @@ -150,7 +150,7 @@ The factory's single entry point is `createB20(variant, salt, params, initCalls) Use `B20FactoryLib` to encode `params` and `initCalls`. Create `script/CreateToken.s.sol`: - ```solidity script/CreateToken.s.sol theme={null} + ```solidity script/CreateToken.s.sol highlight={26} theme={null} // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; @@ -191,10 +191,22 @@ The factory's single entry point is `createB20(variant, salt, params, initCalls) Asset decimals are fixed at creation and must be in `[6, 18]`. The supply cap is optional; the no-cap sentinel is `type(uint128).max` (the cap can never exceed `uint128.max`). + + + Use the `STABLECOIN` variant and its params encoder. A stablecoin fixes decimals at 6 and carries an immutable ISO currency code (uppercase `A`–`Z`) instead of a configurable decimals value: + + ```solidity theme={null} + bytes memory params = B20FactoryLib.encodeStablecoinCreateParams("My USD", "MUSD", account, "USD"); + + token = StdPrecompiles.B20_FACTORY.createB20(IB20Factory.B20Variant.STABLECOIN, salt, params, initCalls); + ``` + + Everything else in this guide — roles, supply cap, minting, and verification — works identically. + - ```bash theme={null} + ```bash Terminal theme={null} source .env base-forge script script/CreateToken.s.sol --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast ``` @@ -205,7 +217,7 @@ The factory's single entry point is `createB20(variant, salt, params, initCalls) If you see `TokenAlreadyExists`, the salt `keccak256("my-first-b20")` is already registered on this network or anvil instance. Either restart `base-anvil` for a fresh state, or change the salt in the script to a unique value. - ```text theme={null} + ```text Output theme={null} == Logs == B20 token created at: 0xB200... ``` @@ -214,7 +226,7 @@ The factory's single entry point is `createB20(variant, salt, params, initCalls) Save the address to an environment variable so the next step needs no copy-paste. The broadcast artifact holds the return value: - ```bash theme={null} + ```bash Terminal theme={null} TOKEN_ADDRESS=$(jq -er '.returns.token.value' \ broadcast/CreateToken.s.sol/$CHAIN_ID/run-latest.json) \ && echo "export TOKEN_ADDRESS=$TOKEN_ADDRESS" >> .env \ @@ -236,7 +248,7 @@ Minting requires `MINT_ROLE`, which `initCalls` granted to your account. - ```bash theme={null} + ```bash Terminal theme={null} base-cast send $TOKEN_ADDRESS "mint(address,uint256)" $ACCOUNT_ADDRESS 1000000000000000000000 \ --rpc-url $RPC_URL --private-key $PRIVATE_KEY ``` @@ -245,13 +257,13 @@ Minting requires `MINT_ROLE`, which `initCalls` granted to your account. - ```bash theme={null} + ```bash Terminal theme={null} base-cast call $TOKEN_ADDRESS "balanceOf(address)(uint256)" $ACCOUNT_ADDRESS --rpc-url $RPC_URL # 1000000000000000000000 [1e21] ``` - The token now holds minted supply on-chain. Search `$TOKEN_ADDRESS` in the explorer to view it. + The token now holds minted supply onchain. Search `$TOKEN_ADDRESS` in the explorer to view it. @@ -263,12 +275,12 @@ In this guide you: * Created a B20 Asset token with one `createB20` call * Configured its admin, minter, and supply cap atomically via `initCalls` * Minted supply -* Verified the balance on-chain +* Verified the balance onchain You did all of this without writing, deploying, or auditing a token contract. ## Next steps -* [Accept B20 payments in an app](/apps/guides/accept-b20-payments): wire this token into a checkout flow that tags each payment with an order ID and reconciles it from on-chain events. +* [Accept B20 payments in an app](/apps/guides/accept-b20-payments): wire this token into a checkout flow that tags each payment with an order ID and reconciles it from onchain events. * Gate transfers or mints with PolicyRegistry policies, add granular pause, or manage roles. See the [B20 token standard](/base-chain/specs/upgrades/beryl/b20). * Issue a stablecoin variant (fixed 6 decimals, immutable currency code). From 996b12f4bbf8185189e9cf468b06e7707547b380 Mon Sep 17 00:00:00 2001 From: Soheima M Date: Thu, 18 Jun 2026 19:17:15 +0200 Subject: [PATCH 7/8] added changes --- docs/apps/guides/accept-b20-payments.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/apps/guides/accept-b20-payments.mdx b/docs/apps/guides/accept-b20-payments.mdx index 51635e716..b6a3f9624 100644 --- a/docs/apps/guides/accept-b20-payments.mdx +++ b/docs/apps/guides/accept-b20-payments.mdx @@ -1,17 +1,17 @@ --- -title: "Accept B20 payments in an app" -description: "Take B20 token payments in your app and match each one to an order with on-chain memos." +title: "Accept B20 payments" +description: "Accept B20 token payments in your app and match each transaction to an order with onchain memos." --- B20 is an ERC-20 superset. Standard `transfer`, `transferFrom`, `approve`, `balanceOf`, and ERC-2612 `permit` all work, so an app that accepts ERC-20 tokens accepts B20 with no code changes. -B20's additions include transfer policies, pausing, supply caps, and memos. This guide uses the memo: `transferWithMemo` works like `transfer`, but also attaches a `bytes32` reference such as an order ID and emits a `Memo` event right after the standard `Transfer`. Read that event to tie each payment to an order. +B20's new features include transfer policies, pausing, supply caps, and memos. This guide uses the memo: `transferWithMemo` works like `transfer`, but also attaches a `bytes32` reference such as an order ID and emits a `Memo` event right after the standard `Transfer`. Read that event to tie each payment to an order. ## Tag a payment with a memo This uses your configured viem `walletClient` and `publicClient`. It reads the token's decimals, sends a payment tagged with an order ID, then reads the memo back from the receipt: -```js theme={null} +```js import { parseUnits, stringToHex, hexToString, parseEventLogs } from 'viem'; const TOKEN = '0xB20f...'; // the B20 token you accept From 6b1fd6b5d35fe7469ca4d4d75eca8a1f707e412c Mon Sep 17 00:00:00 2001 From: Soheima M Date: Thu, 18 Jun 2026 19:21:28 +0200 Subject: [PATCH 8/8] updated wording --- docs/apps/guides/accept-b20-payments.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/apps/guides/accept-b20-payments.mdx b/docs/apps/guides/accept-b20-payments.mdx index b6a3f9624..8681ab8e4 100644 --- a/docs/apps/guides/accept-b20-payments.mdx +++ b/docs/apps/guides/accept-b20-payments.mdx @@ -47,7 +47,7 @@ To collect with an allowance instead of a direct transfer, use `transferFromWith ## Handle B20-specific reverts -A B20 transfer can revert where a plain ERC-20 would not. Surface these so a failed payment is visible, not silent: +A B20 transfer can revert where a standard ERC-20 would not. Surface these so a failed payment is visible, not silent: - `PolicyForbids`: the sender or recipient is not authorized by the token's transfer policy. Most tokens are open by default, but a regulated issuer can gate transfers with an allowlist or blocklist. - A paused transfer: the issuer paused the token's `TRANSFER` feature.