Skip to content

Commit 16a2d18

Browse files
committed
docs: add B20 implementation guides
1 parent 3d3df28 commit 16a2d18

15 files changed

Lines changed: 394 additions & 3 deletions

docs/AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ npx skills add base/base-skills
9696
|base-chain/specs/upgrades/beryl:b20-playground,b20,overview
9797
|base-chain/specs/upgrades/beryl/b20/specification:overview
9898
|base-chain/specs/upgrades/beryl/b20/specification/concepts:architecture-and-precompiles,policies-and-scopes,roles-and-access-control,token-lifecycle,variants-asset-vs-stablecoin
99+
|base-chain/specs/upgrades/beryl/b20/specification/implementation:deployment-and-initcalls-encoding,policy-configuration-in-code,testing-against-precompiles,working-with-base-std
99100
|base-chain/specs/upgrades/canyon:overview
100101
|base-chain/specs/upgrades/cobalt:eip-8130
101102
|base-chain/specs/upgrades/delta:overview,span-batches

docs/base-chain/llms-full.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,18 @@
1717
### Introduction
1818
- [Chain Overview](https://docs.base.org/base-chain/overview.md) — Entry point for Base Chain docs
1919

20+
2021
### B20 Token Standard
2122
- [B20 token standard](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/overview.md) — Base-native ERC-20 superset implemented as Rust precompiles
2223
- [Architecture & precompiles](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/concepts/architecture-and-precompiles.md) — Fixed precompiles, activation gates, and deterministic token addresses
2324
- [Policies & scopes](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/concepts/policies-and-scopes.md) — Token policy slots, PolicyRegistry pointers, built-ins, and composite policies
2425
- [Roles & access control](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/concepts/roles-and-access-control.md) — Built-in roles, user-defined role limits, and admin renunciation
2526
- [Token lifecycle](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/concepts/token-lifecycle.md) — Creation, initCalls, operation, memos, pausing, seizure, and admin renunciation
2627
- [Variants: Asset vs Stablecoin](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/concepts/variants-asset-vs-stablecoin.md) — Variant choice, Asset extras, and Stablecoin currency
28+
- [Working with base-std](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/implementation/working-with-base-std.md) — Install base-std and import B20 interfaces, constants, precompile handles, and helper libraries
29+
- [Policy configuration in code](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/implementation/policy-configuration-in-code.md) — Read, audit, create, and bind policies from Solidity
30+
- [Deployment & initCalls encoding](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/implementation/deployment-and-initcalls-encoding.md) — Use B20FactoryLib to deploy and bootstrap tokens atomically
31+
- [Testing against precompiles](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/implementation/testing-against-precompiles.md) — Test against base-std mocks, base-forge live precompiles, and forked base-anvil nodes
2732

2833
### Core Concepts
2934
- [Transaction Ordering](https://docs.base.org/base-chain/network-information/transaction-ordering.md) — How blocks are ordered on Base

docs/base-chain/llms.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
## Introduction
88
- [Chain Overview](https://docs.base.org/base-chain/overview.md) — Start here for Base Chain docs, including concepts, network reference, node operation, APIs, and protocol specifications.
99

10+
1011
## B20 Token Standard
1112
- [B20 token standard](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/overview.md) — ERC-20-compatible native token standard with policy scopes, roles, memos, pausing, supply caps, and variants
1213
- [Architecture & precompiles](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/concepts/architecture-and-precompiles.md) — Fixed precompiles, activation gates, and deterministic token addresses
1314
- [Policies & scopes](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/concepts/policies-and-scopes.md) — How B20 token scopes point to PolicyRegistry policy IDs
15+
- [Policy configuration in code](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/implementation/policy-configuration-in-code.md) — Read, audit, create, and bind policies from Solidity
16+
- [Deployment & initCalls encoding](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/implementation/deployment-and-initcalls-encoding.md) — Use B20FactoryLib to deploy and bootstrap tokens atomically
1417

1518
## Core Concepts
1619
- [Transaction Ordering](https://docs.base.org/base-chain/network-information/transaction-ordering.md) — How blocks are ordered on Base

docs/base-chain/specs/upgrades/beryl/b20/specification/concepts/architecture-and-precompiles.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,6 @@ Use `IB20Factory.getB20Address(variant, deployer, salt)` to precompute the addre
5151
- Do not expect explorer bytecode verification for token or registry addresses.
5252
- Test with base-std mocks, `base-forge`, or `base-anvil` because stock EVM tooling does not know these precompiles by default.
5353

54+
<CardGroup cols={2}>
55+
<Card title="Work with base-std" href="/base-chain/specs/upgrades/beryl/b20/specification/implementation/working-with-base-std" />
56+
</CardGroup>

docs/base-chain/specs/upgrades/beryl/b20/specification/concepts/policies-and-scopes.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,6 @@ for (uint256 i; i < scopes.length; i++) {
8888
}
8989
```
9090

91+
<CardGroup cols={2}>
92+
<Card title="Configure policies in code" href="/base-chain/specs/upgrades/beryl/b20/specification/implementation/policy-configuration-in-code" />
93+
</CardGroup>

docs/base-chain/specs/upgrades/beryl/b20/specification/concepts/roles-and-access-control.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,6 @@ token.renounceLastAdmin();
6161
For an admin-less launch from creation, put the required grants and policy updates in `initCalls`, then set `initialAdmin` to `address(0)` in the create params.
6262
</Note>
6363

64+
<CardGroup cols={2}>
65+
<Card title="Deployment & initCalls encoding" href="/base-chain/specs/upgrades/beryl/b20/specification/implementation/deployment-and-initcalls-encoding" />
66+
</CardGroup>
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
---
2+
title: "Deployment & initCalls encoding"
3+
description: "Use B20FactoryLib to encode createB20 params and initCalls for atomic B20 token deployment."
4+
---
5+
6+
Use `B20FactoryLib` to produce canonical factory params and initCalls. The factory rejects malformed or unsupported params, so avoid hand-encoding unless you are testing decoder failures.
7+
8+
## Deploy an Asset token
9+
10+
```solidity
11+
// SPDX-License-Identifier: MIT
12+
pragma solidity ^0.8.20;
13+
14+
import {Script} from "forge-std/Script.sol";
15+
import {IB20Factory} from "base-std/interfaces/IB20Factory.sol";
16+
import {B20Constants} from "base-std/lib/B20Constants.sol";
17+
import {B20FactoryLib} from "base-std/lib/B20FactoryLib.sol";
18+
import {StdPrecompiles} from "base-std/StdPrecompiles.sol";
19+
20+
contract DeployB20 is Script {
21+
function run() external returns (address token) {
22+
address admin = vm.envAddress("ADMIN");
23+
address minter = vm.envAddress("MINTER");
24+
bytes32 salt = keccak256("my-token-v1");
25+
26+
bytes memory params = B20FactoryLib.encodeAssetCreateParams("My Token", "MYT", admin, 18);
27+
28+
bytes[] memory initCalls = new bytes[](3);
29+
initCalls[0] = B20FactoryLib.encodeGrantRole(B20Constants.MINT_ROLE, minter);
30+
initCalls[1] = B20FactoryLib.encodeGrantRole(B20Constants.PAUSE_ROLE, admin);
31+
initCalls[2] = B20FactoryLib.encodeUpdateSupplyCap(1_000_000e18);
32+
33+
vm.broadcast();
34+
token = StdPrecompiles.B20_FACTORY.createB20(
35+
IB20Factory.B20Variant.ASSET,
36+
salt,
37+
params,
38+
initCalls
39+
);
40+
}
41+
}
42+
```
43+
44+
## Pre-derive the token address
45+
46+
Some initCalls or offchain systems need the token address before deployment.
47+
48+
```solidity
49+
address predicted = StdPrecompiles.B20_FACTORY.getB20Address(
50+
IB20Factory.B20Variant.ASSET,
51+
deployer,
52+
salt
53+
);
54+
```
55+
56+
After creation, verify with:
57+
58+
```solidity
59+
require(StdPrecompiles.B20_FACTORY.isB20(predicted), "not B20-shaped");
60+
require(StdPrecompiles.B20_FACTORY.isB20Initialized(predicted), "not initialized");
61+
```
62+
63+
## initCalls rules
64+
65+
During initCalls, factory-originated calls bypass role gates and transfer-side policy gates. They do not bypass:
66+
67+
- `MINT_RECEIVER_POLICY`
68+
- Pause state
69+
- Supply cap or other accounting invariants
70+
71+
That means ordering matters. Configure mint receiver policy before minting only if the mint recipient is authorized by that policy.
72+
73+
## Admin-less deployment
74+
75+
To deploy admin-less from block one, set `initialAdmin` to `address(0)` and put all required role grants, policy bindings, and cap configuration into initCalls.
76+
77+
```solidity
78+
bytes memory params = B20FactoryLib.encodeAssetCreateParams("Adminless", "ADL", address(0), 18);
79+
80+
bytes[] memory initCalls = new bytes[](3);
81+
initCalls[0] = B20FactoryLib.encodeGrantRole(B20Constants.MINT_ROLE, minter);
82+
initCalls[1] = B20FactoryLib.encodeUpdatePolicy(B20Constants.MINT_RECEIVER_POLICY, mintPolicyId);
83+
initCalls[2] = B20FactoryLib.encodeUpdateSupplyCap(1_000_000e18);
84+
```
85+
86+
<Warning>
87+
There is no later admin recovery path for an admin-less token. Missing policies or roles cannot be added after deployment.
88+
</Warning>
89+
90+
<CardGroup cols={2}>
91+
<Card title="Token lifecycle" href="/base-chain/specs/upgrades/beryl/b20/specification/concepts/token-lifecycle" />
92+
</CardGroup>
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
---
2+
title: "Policy configuration in code"
3+
description: "Read, audit, create, update, and bind B20 PolicyRegistry policies from Solidity."
4+
---
5+
6+
B20 policy configuration has two parts:
7+
8+
1. Read each token scope with `token.policyId(scope)`.
9+
2. Read or write the pointed-to policy in the singleton PolicyRegistry.
10+
11+
## Audit a token's policy scopes
12+
13+
```solidity
14+
// SPDX-License-Identifier: MIT
15+
pragma solidity ^0.8.20;
16+
17+
import {Script, console2} from "forge-std/Script.sol";
18+
import {IB20} from "base-std/interfaces/IB20.sol";
19+
import {IPolicyRegistry} from "base-std/interfaces/IPolicyRegistry.sol";
20+
import {B20Constants} from "base-std/lib/B20Constants.sol";
21+
import {StdPrecompiles} from "base-std/StdPrecompiles.sol";
22+
23+
contract AuditB20Policies is Script {
24+
bytes32[5] internal scopes = [
25+
B20Constants.TRANSFER_SENDER_POLICY,
26+
B20Constants.TRANSFER_RECEIVER_POLICY,
27+
B20Constants.TRANSFER_EXECUTOR_POLICY,
28+
B20Constants.MINT_RECEIVER_POLICY,
29+
B20Constants.SEIZE_HOLDER_POLICY
30+
];
31+
32+
function run(address tokenAddress, address accountToCheck) external view {
33+
IB20 token = IB20(tokenAddress);
34+
IPolicyRegistry registry = StdPrecompiles.POLICY_REGISTRY;
35+
36+
for (uint256 i; i < scopes.length; i++) {
37+
uint64 id = token.policyId(scopes[i]);
38+
console2.logBytes32(scopes[i]);
39+
console2.log("policyId", id);
40+
console2.log("exists", id == 0 || registry.policyExists(id));
41+
console2.log("authorized", registry.isAuthorized(id, accountToCheck));
42+
console2.log("admin", registry.policyAdmin(id));
43+
console2.log("pendingAdmin", registry.pendingPolicyAdmin(id));
44+
}
45+
}
46+
}
47+
```
48+
49+
Interpretation:
50+
51+
| Value | Meaning |
52+
|---|---|
53+
| `0` | `ALWAYS_ALLOW`; the scope is wide open. |
54+
| Top byte `0` | `BLOCKLIST`; empty/uncreated behaves authorized by default. |
55+
| Top byte `1` | `ALLOWLIST`; empty/uncreated behaves denied by default. |
56+
| Top byte `2` | `UNION`; composite policy. |
57+
| Top byte `3` | `INTERSECT`; composite policy. |
58+
59+
<Warning>
60+
Validate `policyExists(policyId)` before binding a scope. `isAuthorized` does not revert for missing IDs.
61+
</Warning>
62+
63+
## Create and bind a simple policy
64+
65+
```solidity
66+
IPolicyRegistry registry = StdPrecompiles.POLICY_REGISTRY;
67+
IB20 token = IB20(tokenAddress);
68+
69+
address[] memory initialMembers = new address[](1);
70+
initialMembers[0] = treasury;
71+
72+
uint64 mintAllowlist = registry.createPolicyWithAccounts(
73+
policyAdmin,
74+
IPolicyRegistry.PolicyType.ALLOWLIST,
75+
initialMembers
76+
);
77+
78+
require(registry.policyExists(mintAllowlist), "policy missing");
79+
token.updatePolicy(B20Constants.MINT_RECEIVER_POLICY, mintAllowlist);
80+
```
81+
82+
## Update membership
83+
84+
```solidity
85+
address[] memory accounts = new address[](2);
86+
accounts[0] = alice;
87+
accounts[1] = bob;
88+
89+
// ALLOWLIST: true adds authorization; false removes it.
90+
registry.updateAllowlist(mintAllowlist, true, accounts);
91+
92+
// BLOCKLIST: true blocks; false unblocks.
93+
registry.updateBlocklist(transferBlocklist, true, accounts);
94+
```
95+
96+
## Create a composite policy
97+
98+
```solidity
99+
uint64[] memory children = new uint64[](2);
100+
children[0] = kycAllowlist;
101+
children[1] = sanctionsBlocklist;
102+
103+
uint64 policyId = registry.createCompositePolicy(
104+
policyAdmin,
105+
IPolicyRegistry.PolicyType.INTERSECT,
106+
children
107+
);
108+
109+
token.updatePolicy(B20Constants.TRANSFER_RECEIVER_POLICY, policyId);
110+
```
111+
112+
## Transfer or freeze policy administration
113+
114+
```solidity
115+
// Current admin stages a transfer.
116+
registry.stageUpdateAdmin(policyId, newAdmin);
117+
118+
// Pending admin accepts it.
119+
vm.prank(newAdmin);
120+
registry.finalizeUpdateAdmin(policyId);
121+
122+
// Irreversible: freezes policy membership forever.
123+
registry.renounceAdmin(policyId);
124+
```
125+
126+
<CardGroup cols={2}>
127+
<Card title="Policies & scopes" href="/base-chain/specs/upgrades/beryl/b20/specification/concepts/policies-and-scopes" />
128+
</CardGroup>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
title: "Testing against precompiles"
3+
description: "Test B20 code with base-std mocks, base-forge in-process live precompiles, and forked base-anvil nodes."
4+
---
5+
6+
B20 precompile tests run in three modes. Pick the lowest-cost mode that catches the behavior you need.
7+
8+
## Mode 1: Solidity mocks with stock Forge
9+
10+
Use this for fast unit tests that do not need the Rust precompile backend.
11+
12+
```bash
13+
forge test
14+
```
15+
16+
`base-std` provides mocks under `test/lib/mocks/`. Test bases can etch these mocks at the fixed precompile addresses so calls to `StdPrecompiles` work in stock Foundry.
17+
18+
## Mode 2: Live precompiles in-process with base-forge
19+
20+
Use this to run against the Rust precompile implementation without running a node.
21+
22+
```bash
23+
curl -L https://raw.githubusercontent.com/base/base-anvil/HEAD/foundryup/install | bash
24+
base-foundryup
25+
base-forge test
26+
```
27+
28+
`base-forge` hosts the precompiles inside Forge's EVM and seeds gated features active for the no-node path. `BaseTest` in `base-std` auto-detects whether it is running live precompiles or reference mocks.
29+
30+
## Mode 3: Forked base-anvil node
31+
32+
Use this when you need genuine RPC/fork behavior against a local node.
33+
34+
```bash
35+
make smoke-setup
36+
ANVIL_BIN="$HOME/.foundry/versions/base-nightly/anvil" \
37+
FORGE_BIN="$HOME/.foundry/versions/base-nightly/forge" \
38+
make fork-tests
39+
```
40+
41+
A `base-anvil` node starts with gated features inactive, like a real chain before feature activation. Activate the needed features before testing deployment or state-changing registry paths.
42+
43+
## Why LIVE_PRECOMPILES matters
44+
45+
When a test etches mock bytecode at a precompile address, the EVM executes the etched bytecode before consulting native precompile dispatch. If you enable Base precompile dispatch but still etch mocks, tests can falsely pass against Solidity mocks while you think they are using Rust precompiles.
46+
47+
Use `LIVE_PRECOMPILES=true` in fork profiles that should skip mock etching and call the native backend.
48+
49+
```bash
50+
LIVE_PRECOMPILES=true FOUNDRY_PROFILE=fork forge test --fork-url http://localhost:8546
51+
```
52+
53+
## Failure diagnosis
54+
55+
1. **Activation:** Did the chain activate the relevant B20 or registry feature?
56+
2. **Deployment:** Is the target address a fixed precompile or an initialized B20 token?
57+
3. **Divergence:** If mocks pass and live precompiles fail, compare the failing selector, storage slot, and event/revert order against `base-std` tests.
58+
59+
<CardGroup cols={2}>
60+
<Card title="Working with base-std" href="/base-chain/specs/upgrades/beryl/b20/specification/implementation/working-with-base-std" />
61+
<Card title="Architecture & precompiles" href="/base-chain/specs/upgrades/beryl/b20/specification/concepts/architecture-and-precompiles" />
62+
</CardGroup>

0 commit comments

Comments
 (0)