Solidity interfaces, libraries, and reference mocks for Base's precompiles: B20 (ERC-20 superset with roles, policies, pausing, permits, memos), PolicyRegistry (allowlist/blocklist singleton), and ActivationRegistry (feature flags). The Solidity mocks must mirror the Rust implementations in base/base slot-for-slot — storage layout parity is the core invariant of this repo.
forge build # compile (solc 0.8.30)
forge test -v # unit tests against mocked precompiles (fast; run first)
forge test -v --match-test <name> # one test; --match-contract <name> for one suite
forge fmt # format Solidity (CI gates on `forge fmt --check`)
python3 script/check-coverage.py # every interface function has a test (CI gate)
make coverage # lcov + HTML report (needs genhtml)
base-forge test # same suite vs the real Rust precompiles, in-process (no node); auto-detects
make smoke-setup # one-time venv setup — requires Python 3.13 exactly
make smoke-all # live-RPC smoke journeys (needs .env; see below)
make fork-tests # same suite vs a real base-anvil node (CI harness; patched anvil/forge)Test tiers, in the order to reach for them: unit (mocks, no network) → live precompile
(real Rust precompiles — in-process via base-forge test, or vs a node via make fork-tests;
cross-validates layout/behavior) → smoke (real txs against a live chain).
- Unit tests live in
test/unit/{Feature}/{Name}.t.sol; names followtest_{function}_{outcome}_{variant}, e.g.test_allowance_success_zeroByDefault. - Tests assert precompile storage directly via
vm.loadagainst the slot constants intest/lib/mocks/MockB20Storage.sol. A live-precompile failure with passing unit tests means the Solidity reference and the Rust impl have diverged — seeLIVE_PRECOMPILE_TESTING.md, don't just patch the test. - Live-precompile tests need the patched binaries from
base/base-anvil; stock forge/anvil will not work. Install
them alongside stock Foundry with
base-foundryup(it never touches yourforge/anvil). Common path — in-process, no node:base-forge test;BaseTestauto-detects andsetUplogs LIVE PRECOMPILE mode vs REFERENCE mode. CI/node path:make fork-testsbootsanvil --baseand activates the gated features for you (override the binaries withANVIL_BIN/FORGE_BIN). Pass forge args viamake fork-tests ARGS="-vvvv --match-test <name>". Details:LIVE_PRECOMPILE_TESTING.md,script/fork/README.md. - Smoke tests need
.env(copy.env.template):RPC_URL,DEPLOYER_PK,USER2_PK— testnet keys only, both accounts funded. Journeys skip (not fail) when the target chain hasn't activated the feature — a skip is not a pass. Run one journey withmake smoke-{factory,asset,stablecoin,policy,invariants}; audit mode:make smoke-all KEEP_GOING=1. - Fuzz runs: 256 default, 10 under
FOUNDRY_PROFILE=fork(RPC round-trips are slow).
src/StdPrecompiles.sol # canonical precompile addresses + typed handles
src/interfaces/ # IB20, IB20Asset, IB20Stablecoin, IB20Factory, IPolicyRegistry, IActivationRegistry
src/lib/ # B20Constants (role/policy ids), B20FactoryLib (createB20 encoders)
src/impls/ # reserved for reference impls (currently empty; mocks fill that role)
test/unit/ # one directory per feature; slot-level assertions
test/regression/ # interface renames/removals guard (B20Renames.t.sol, B20Removals.t.sol)
test/lib/ # BaseTest.sol, B20Test.sol, mocks/ (reference behavior + storage layout)
script/smoke/ # Python 3.13 live-node smoketest (web3.py); see script/smoke/README.md
script/fork/ # node-based live-precompile runner (anvil + patched forge); see script/fork/README.md
docs/ # specs: docs/B20/README.md, docs/PolicyRegistry/, docs/ActivationRegistry/
Deeper reading: LIVE_PRECOMPILE_TESTING.md (cross-validation architecture), docs/B20/README.md (B20 spec).
- Solidity formatting comes from
foundry.toml: 120-char lines, 4-space tabs, double quotes, long int types (uint256, neveruint). Runforge fmtbefore committing. - Interfaces use pragma
>=0.8.20 <0.9.0(consumer compatibility); test/mock code uses^0.8.20, compiled with the pinned solc 0.8.30 fromfoundry.toml. - Errors are custom types with parameters, e.g.
error InvalidSupplyCap(uint256 currentSupply, uint256 proposedCap)— neverrequirestrings. - Import via the public remappings
base-std/=src/andbase-std-test/=test/. - Mock state uses ERC-7201 namespaced storage; new state must follow the same pattern.
- Python (
script/): 3.13, PEP 8, snake_case functions, frozen dataclasses for config.web3>=7.6,<8is the only dependency; both runners sharescript/smoke/.venv.
mainis the default branch. Branch frommainand open PRs againstmain.- Conventional Commits with optional scope:
feat(b20): ...,fix(smoke): ...,test:,docs:,chore:. Put the rationale in the body, not just the what. - CI on every PR:
forge build,forge test,forge fmt --check,python3 script/check-coverage.py, coverage comment. Live-precompile tests run in a separate workflow.
- Don't change the precompile addresses in
src/StdPrecompiles.solor the feature IDs inscript/smoke/config.py/test/lib/mocks/ActivationRegistryFeatureList.sol— they are canonical constants shared with base/base; coordinate changes there first. - Don't reshape mock storage layout unilaterally — match
MockB20Storage.solslot constants and cross-validate withmake fork-testsbefore merging. - Don't commit
.envor real private keys —.envis gitignored; use funded testnet keys. - Don't hand-edit ABIs for the smoke tests —
script/smoke/abis.pyloads them fromout/; runforge buildinstead. - Don't weaken a slot-assertion test to make live-precompile tests pass — investigate the divergence via
LIVE_PRECOMPILE_TESTING.mdand fix the side that's wrong.