diff --git a/docs/base-chain/specs/upgrades/beryl/b20/specification/implementation/deployment-and-initcalls-encoding.mdx b/docs/base-chain/specs/upgrades/beryl/b20/specification/implementation/deployment-and-initcalls-encoding.mdx new file mode 100644 index 000000000..335f0007e --- /dev/null +++ b/docs/base-chain/specs/upgrades/beryl/b20/specification/implementation/deployment-and-initcalls-encoding.mdx @@ -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); +``` + + +There is no later admin recovery path for an admin-less token. Missing policies or roles cannot be added after deployment. + + + + + diff --git a/docs/base-chain/specs/upgrades/beryl/b20/specification/implementation/policy-configuration-in-code.mdx b/docs/base-chain/specs/upgrades/beryl/b20/specification/implementation/policy-configuration-in-code.mdx new file mode 100644 index 000000000..aec3dc6bf --- /dev/null +++ b/docs/base-chain/specs/upgrades/beryl/b20/specification/implementation/policy-configuration-in-code.mdx @@ -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 = [ + 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. | + + +Validate `policyExists(policyId)` before binding a scope. `isAuthorized` does not revert for missing IDs. + + +## 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); +``` + + + + diff --git a/docs/base-chain/specs/upgrades/beryl/b20/specification/implementation/testing-against-precompiles.mdx b/docs/base-chain/specs/upgrades/beryl/b20/specification/implementation/testing-against-precompiles.mdx new file mode 100644 index 000000000..fbd825d11 --- /dev/null +++ b/docs/base-chain/specs/upgrades/beryl/b20/specification/implementation/testing-against-precompiles.mdx @@ -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. + + + + + diff --git a/docs/base-chain/specs/upgrades/beryl/b20/specification/implementation/working-with-base-std.mdx b/docs/base-chain/specs/upgrades/beryl/b20/specification/implementation/working-with-base-std.mdx new file mode 100644 index 000000000..876f335d0 --- /dev/null +++ b/docs/base-chain/specs/upgrades/beryl/b20/specification/implementation/working-with-base-std.mdx @@ -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. + + + +