Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
---
title: "Deployment & initCalls encoding"
description: "Use B20FactoryLib to encode createB20 params and initCalls for atomic B20 token deployment."
---

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.

## Deploy an Asset token

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Script} from "forge-std/Script.sol";
import {IB20Factory} from "base-std/interfaces/IB20Factory.sol";
import {B20Constants} from "base-std/lib/B20Constants.sol";
import {B20FactoryLib} from "base-std/lib/B20FactoryLib.sol";
import {StdPrecompiles} from "base-std/StdPrecompiles.sol";

contract DeployB20 is Script {
function run() external returns (address token) {
address admin = vm.envAddress("ADMIN");
address minter = vm.envAddress("MINTER");
bytes32 salt = keccak256("my-token-v1");

bytes memory params = B20FactoryLib.encodeAssetCreateParams("My Token", "MYT", admin, 18);

bytes[] memory initCalls = new bytes[](3);
initCalls[0] = B20FactoryLib.encodeGrantRole(B20Constants.MINT_ROLE, minter);
initCalls[1] = B20FactoryLib.encodeGrantRole(B20Constants.PAUSE_ROLE, admin);
initCalls[2] = B20FactoryLib.encodeUpdateSupplyCap(1_000_000e18);

vm.broadcast();
token = StdPrecompiles.B20_FACTORY.createB20(
IB20Factory.B20Variant.ASSET,
salt,
params,
initCalls
);
}
}
```

## Pre-derive the token address

Some initCalls or offchain systems need the token address before deployment.

```solidity
address predicted = StdPrecompiles.B20_FACTORY.getB20Address(
IB20Factory.B20Variant.ASSET,
deployer,
salt
);
```

After creation, verify with:

```solidity
require(StdPrecompiles.B20_FACTORY.isB20(predicted), "not B20-shaped");
require(StdPrecompiles.B20_FACTORY.isB20Initialized(predicted), "not initialized");
```

## initCalls rules

During initCalls, factory-originated calls bypass role gates and transfer-side policy gates. They do not bypass:

- `MINT_RECEIVER_POLICY`
- Pause state
- Supply cap or other accounting invariants

That means ordering matters. Configure mint receiver policy before minting only if the mint recipient is authorized by that policy.

## Admin-less deployment

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.

```solidity
bytes memory params = B20FactoryLib.encodeAssetCreateParams("Adminless", "ADL", address(0), 18);

bytes[] memory initCalls = new bytes[](3);
initCalls[0] = B20FactoryLib.encodeGrantRole(B20Constants.MINT_ROLE, minter);
initCalls[1] = B20FactoryLib.encodeUpdatePolicy(B20Constants.MINT_RECEIVER_POLICY, mintPolicyId);
initCalls[2] = B20FactoryLib.encodeUpdateSupplyCap(1_000_000e18);
```

<Warning>
There is no later admin recovery path for an admin-less token. Missing policies or roles cannot be added after deployment.
</Warning>

<CardGroup cols={2}>
<Card title="Token lifecycle" href="/base-chain/specs/upgrades/beryl/b20/specification/concepts/token-lifecycle" />
</CardGroup>
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
title: "Policy configuration in code"
description: "Read, audit, create, update, and bind B20 PolicyRegistry policies from Solidity."
---

B20 policy configuration has two parts:

1. Read each token scope with `token.policyId(scope)`.
2. Read or write the pointed-to policy in the singleton PolicyRegistry.

## Audit a token's policy scopes

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Script, console2} from "forge-std/Script.sol";
import {IB20} from "base-std/interfaces/IB20.sol";
import {IPolicyRegistry} from "base-std/interfaces/IPolicyRegistry.sol";
import {B20Constants} from "base-std/lib/B20Constants.sol";
import {StdPrecompiles} from "base-std/StdPrecompiles.sol";

contract AuditB20Policies is Script {
bytes32[5] internal scopes = [

@roethke roethke Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bytes32[5] scopes array omits SEIZE_RECEIVER_POLICY, a real 6th policy scope (B20Constants.sol, IB20.sol:274). As written, the AuditB20Policies sample silently skips a scope, so anyone copying it would under-audit. Make it bytes32[6] and add B20Constants.SEIZE_RECEIVER_POLICY.

B20Constants.TRANSFER_SENDER_POLICY,
B20Constants.TRANSFER_RECEIVER_POLICY,
B20Constants.TRANSFER_EXECUTOR_POLICY,
B20Constants.MINT_RECEIVER_POLICY,
B20Constants.SEIZE_HOLDER_POLICY
];

function run(address tokenAddress, address accountToCheck) external view {
IB20 token = IB20(tokenAddress);
IPolicyRegistry registry = StdPrecompiles.POLICY_REGISTRY;

for (uint256 i; i < scopes.length; i++) {
uint64 id = token.policyId(scopes[i]);
console2.logBytes32(scopes[i]);
console2.log("policyId", id);
console2.log("exists", id == 0 || registry.policyExists(id));
console2.log("authorized", registry.isAuthorized(id, accountToCheck));
console2.log("admin", registry.policyAdmin(id));
console2.log("pendingAdmin", registry.pendingPolicyAdmin(id));
}
}
}
```

Interpretation:

| Value | Meaning |
|---|---|
| `0` | `ALWAYS_ALLOW`; the scope is wide open. |
| Top byte `0` | `BLOCKLIST`; empty/uncreated behaves authorized by default. |
| Top byte `1` | `ALLOWLIST`; empty/uncreated behaves denied by default. |
| Top byte `2` | `UNION`; composite policy. |
| Top byte `3` | `INTERSECT`; composite policy. |

<Warning>
Validate `policyExists(policyId)` before binding a scope. `isAuthorized` does not revert for missing IDs.
</Warning>

## Create and bind a simple policy

```solidity
IPolicyRegistry registry = StdPrecompiles.POLICY_REGISTRY;
IB20 token = IB20(tokenAddress);

address[] memory initialMembers = new address[](1);
initialMembers[0] = treasury;

uint64 mintAllowlist = registry.createPolicyWithAccounts(
policyAdmin,
IPolicyRegistry.PolicyType.ALLOWLIST,
initialMembers
);

require(registry.policyExists(mintAllowlist), "policy missing");
token.updatePolicy(B20Constants.MINT_RECEIVER_POLICY, mintAllowlist);
```

## Update membership

```solidity
address[] memory accounts = new address[](2);
accounts[0] = alice;
accounts[1] = bob;

// ALLOWLIST: true adds authorization; false removes it.
registry.updateAllowlist(mintAllowlist, true, accounts);

// BLOCKLIST: true blocks; false unblocks.
registry.updateBlocklist(transferBlocklist, true, accounts);
```

## Create a composite policy

```solidity
uint64[] memory children = new uint64[](2);
children[0] = kycAllowlist;
children[1] = sanctionsBlocklist;

uint64 policyId = registry.createCompositePolicy(
policyAdmin,
IPolicyRegistry.PolicyType.INTERSECT,
children
);

token.updatePolicy(B20Constants.TRANSFER_RECEIVER_POLICY, policyId);
```

## Transfer or freeze policy administration

```solidity
// Current admin stages a transfer.
registry.stageUpdateAdmin(policyId, newAdmin);

// Pending admin accepts it.
vm.prank(newAdmin);
registry.finalizeUpdateAdmin(policyId);

// Irreversible: freezes policy membership forever.
registry.renounceAdmin(policyId);
```

<CardGroup cols={2}>
<Card title="Policies & scopes" href="/base-chain/specs/upgrades/beryl/b20/specification/concepts/policies-and-scopes" />
</CardGroup>
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: "Testing against precompiles"
description: "Test B20 code with base-std mocks, base-forge in-process live precompiles, and forked base-anvil nodes."
---

B20 precompile tests run in three modes. Pick the lowest-cost mode that catches the behavior you need.

## Mode 1: Solidity mocks with stock Forge

Use this for fast unit tests that do not need the Rust precompile backend.

```bash
forge test
```

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

## Mode 2: Live precompiles in-process with base-forge

Use this to run against the Rust precompile implementation without running a node.

```bash
curl -L https://raw.githubusercontent.com/base/base-anvil/HEAD/foundryup/install | bash
base-foundryup
base-forge test
```

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

## Mode 3: Forked base-anvil node

Use this when you need genuine RPC/fork behavior against a local node.

```bash
make smoke-setup
ANVIL_BIN="$HOME/.foundry/versions/base-nightly/anvil" \
FORGE_BIN="$HOME/.foundry/versions/base-nightly/forge" \
make fork-tests
```

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.

## Why LIVE_PRECOMPILES matters

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.

Use `LIVE_PRECOMPILES=true` in fork profiles that should skip mock etching and call the native backend.

```bash
LIVE_PRECOMPILES=true FOUNDRY_PROFILE=fork forge test --fork-url http://localhost:8546
```

## Failure diagnosis

1. **Activation:** Did the chain activate the relevant B20 or registry feature?
2. **Deployment:** Is the target address a fixed precompile or an initialized B20 token?
3. **Divergence:** If mocks pass and live precompiles fail, compare the failing selector, storage slot, and event/revert order against `base-std` tests.

<CardGroup cols={2}>
<Card title="Working with base-std" href="/base-chain/specs/upgrades/beryl/b20/specification/implementation/working-with-base-std" />
<Card title="Architecture & precompiles" href="/base-chain/specs/upgrades/beryl/b20/specification/concepts/architecture-and-precompiles" />
</CardGroup>
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: "Working with base-std"
description: "Install base-std and use StdPrecompiles, B20Constants, interfaces, and helper libraries for B20 development."
---

`base-std` is the Solidity source of truth for B20 interfaces, constants, factory encoders, and test mocks.

## Install

```bash
forge install base/base-std
```

Add remappings for source and test helpers:

```toml foundry.toml
remappings = [
"base-std/=lib/base-std/src/",
"base-std-test/=lib/base-std/test/"
]
```

Interfaces target `>=0.8.20 <0.9.0`. Reference implementations and tests in `base-std` pin Solidity `0.8.30`.

## Use StdPrecompiles

`StdPrecompiles.sol` exposes fixed precompile addresses and typed handles.

```solidity
import {StdPrecompiles} from "base-std/StdPrecompiles.sol";
import {IB20Factory} from "base-std/interfaces/IB20Factory.sol";

address predicted = StdPrecompiles.B20_FACTORY.getB20Address(
IB20Factory.B20Variant.ASSET,
deployer,
salt
);
```

## Use canonical constants

Use `B20Constants.sol` instead of redefining role and scope hashes.

```solidity
import {B20Constants} from "base-std/lib/B20Constants.sol";

bytes32 mintRole = B20Constants.MINT_ROLE;
bytes32 mintScope = B20Constants.MINT_RECEIVER_POLICY;
```

## Where things live

| Path | Purpose |
|---|---|
| `src/StdPrecompiles.sol` | Fixed precompile addresses and typed handles |
| `src/interfaces/` | Solidity interfaces for B20, variants, factory, and registries |
| `src/lib/B20Constants.sol` | Role, policy-scope, feature, decimal, and supply-cap constants |
| `src/lib/B20FactoryLib.sol` | Pure encoders for factory params and initCalls |
| `test/lib/mocks/` | Solidity mocks for stock Foundry tests |

## What not to do

- Do not hardcode precompile addresses in application code when `StdPrecompiles` is available.
- Do not copy interface files into your repo; import them to avoid version skew.
- Do not assume explorer verification exists for B20 precompile token addresses.
- Do not use stock `forge` alone when your test expects live precompile behavior.

<CardGroup cols={2}>
<Card title="Testing against precompiles" href="/base-chain/specs/upgrades/beryl/b20/specification/implementation/testing-against-precompiles" />
</CardGroup>
Loading