Skip to main content
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 onchain. To accept the token as payment in an app, continue with Accept B20 payments.

Before you begin

You need Base’s Foundry build (base-forge, base-cast, base-anvil). Install it via base-foundryup:
Terminal
curl -L https://raw.githubusercontent.com/base/base-anvil/HEAD/foundryup/install | bash
base-foundryup --install v1.1.0
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

Terminal
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:
foundry.toml
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.

Choose a network

Pick a network with the B20 precompiles active, then create a .env inside your b20-quickstart project directory:
SettingValue
RPC URLhttps://sepolia.base.org
Chain ID84532
FaucetCDP Faucet or other providers
Explorersepolia.basescan.org
.env
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:
Terminal
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.

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

Write the create script

Use B20FactoryLib to encode params and initCalls. Create script/CreateToken.s.sol:
script/CreateToken.s.sol
// 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).
Use the STABLECOIN variant and its params encoder. A stablecoin fixes decimals at 6 and carries an immutable ISO currency code (uppercase AZ) instead of a configurable decimals value:
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.
2

Deploy the factory call

Terminal
source .env
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.
Output
== Logs ==
  B20 token created at: 0xB200...
3

Capture the token address

Save the address to an environment variable so the next step needs no copy-paste. The broadcast artifact holds the return value:
Terminal
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.

Mint and verify

Minting requires MINT_ROLE, which initCalls granted to your account.
1

Mint supply

Terminal
base-cast send $TOKEN_ADDRESS "mint(address,uint256)" $ACCOUNT_ADDRESS 1000000000000000000000 \
  --rpc-url $RPC_URL --private-key $PRIVATE_KEY
base-cast send prints a receipt with status 1 (success).
2

Confirm the balance

Terminal
base-cast call $TOKEN_ADDRESS "balanceOf(address)(uint256)" $ACCOUNT_ADDRESS --rpc-url $RPC_URL
# 1000000000000000000000 [1e21]
The token now holds minted supply onchain. Search $TOKEN_ADDRESS in the explorer 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
  • 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: 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.
  • Issue a stablecoin variant (fixed 6 decimals, immutable currency code).