Skip to content
Merged
8 changes: 4 additions & 4 deletions FORK_TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
## What this does

Runs base-std's existing unit test suite (~346 tests with paired
`vm.load`-based slot assertions, from PR #43) against a **local node that
`vm.load`-based slot assertions) against a **local node that
hosts Base's Rust precompiles** (the patched `anvil` from the base-anvil
fork). Forge dispatches calls to those precompiles instead of to base-std's
Solidity mocks; the suite's slot-level assertions then surface any
Expand Down Expand Up @@ -216,7 +216,7 @@ matching `FEATURE_*` constant).

**Build fails with `error[E0599]: no variant or associated item named B20_STABLECOIN`
in `b20_stablecoin/dispatch.rs`** — you bumped past base/base commit
`d7662c05e` (PR #2834, "replace feature id constants with ActivationFeature
`d7662c05e` ("replace feature id constants with ActivationFeature
enum"). That refactor removed `ActivationRegistryStorage::B20_STABLECOIN`
but the call site in `dispatch.rs` still references it. Until upstream
fixes this, either pin the dep back to a pre-d7662c05e commit (e.g.
Expand Down Expand Up @@ -249,15 +249,15 @@ If this returns garbage / fails, the fork's build is broken or out of date.
| The test runner script | `script/run-fork-tests.sh` | bash; takes forge args through `$@` |
| Forge profile config | `foundry.toml`, `[profile.fork]` | `base = true` enables Rust precompile dispatch |
| Skip-etch logic | `test/lib/BaseTest.sol` | guarded by `LIVE_PRECOMPILES` env var |
| Slot assertions | `test/unit/**/*.t.sol`, `test/lib/mocks/Mock*Storage.sol` | shipped in base-std PR #43 |
| Slot assertions | `test/unit/**/*.t.sol`, `test/lib/mocks/Mock*Storage.sol` | `vm.load`-based slot-layout assertions paired with surface tests |
| Storage helpers | `test/lib/mocks/MockB20Storage.sol`, `MockPolicyRegistryStorage.sol` | the slot-derivation library every assertion uses |
| Patched forge + anvil | `~/code/base-anvil/target/.../{forge,anvil}` | built by `cargo build -p forge -p anvil` |
| `--base` flag implementation | `~/code/base-anvil/crates/evm/networks/src/lib.rs` | edit here when precompile set changes |
| base/base git pin | `~/code/base-anvil/crates/evm/networks/Cargo.toml` | the `rev = "..."` line, bumped via `./script/bump-base.sh` |
| Local-iteration override | `~/code/base-anvil/Cargo.toml` | commented `[patch."https://github.com/base/base.git"]` block |
| Rust precompile source | `github.com/base/base`, pinned commit | fetched by cargo on build; optional local clone for [patch] |
| Feature IDs | `base/crates/common/precompiles/src/activation/storage.rs` (on github) | `FEATURE_*` consts |
| ActivationRegistry default admin | `0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc` | codified in base/base PR #2811 |
| ActivationRegistry default admin | `0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc` | the canonical local-dev admin |
| Vibenet chainid | 84538453 | auto-enables `--base` |

## What's NOT in scope
Expand Down
2 changes: 1 addition & 1 deletion docs/B20/Asset.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Read the current multiplier with `multiplier()`; the value is in WAD precision (

## Announcements

Announcements are publicly viewable notifications posted by a token operator. They can represent anything the operator wants to create a record of and can be coupled with actual state changes on the token (updating the multiplier, batched mints/burns, and so on).
Announcements are publicly viewable notifications posted by a token operator. They can represent anything the operator wants to create a record of and can be coupled with actual state changes on the token (updating the multiplier, batched mints, and so on).

### Event Topology

Expand Down
Binary file added script/__pycache__/mutate.cpython-312.pyc
Binary file not shown.
2 changes: 1 addition & 1 deletion script/mutate.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class Mutation:
" // mint receiver policy check elided\n if (false) revert PolicyForbids(MINT_RECEIVER_POLICY, mintReceiverPolicyId);",
"_mint: drop MINT_RECEIVER_POLICY policy check entirely",
),
# === MockB20: lastAdmin guard off-by-one (post-#40 inline conjunction) ===
# === MockB20: lastAdmin guard off-by-one (inline conjunction) ===
Mutation(
MOCK_B20,
"if (role == DEFAULT_ADMIN_ROLE && $.roles[DEFAULT_ADMIN_ROLE][msg.sender] && $.adminCount == 1) {",
Expand Down
12 changes: 5 additions & 7 deletions script/run-fork-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
# PORT local RPC port for anvil (default: 8546)
# ACTIVATION_ADMIN address authorized to activate features
# (default: 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc, the
# canonical local-dev admin codified in base/base#2811)
# canonical local-dev admin)
# ANVIL_LOG anvil stdout/stderr log path (default: /tmp/anvil.log)
#
# Exit codes:
Expand Down Expand Up @@ -76,14 +76,12 @@ ACTIVATION_ADMIN="${ACTIVATION_ADMIN:-0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc
REGISTRY=0x8453000000000000000000000000000000000001
LOG_FILE="${ANVIL_LOG:-/tmp/anvil.log}"

# Feature IDs from base/base/crates/common/precompiles/src/activation/storage.rs.
# If a new feature is added there, append its ID here.
# Feature IDs mirror the canonical set in test/lib/mocks/ActivationRegistryFeatureList.sol
# (the Solidity reference is the source of truth). If a feature is added there, append its ID here.
FEATURE_IDS=(
0x78751e29c8bcc0d609ab18e9fbc4158e73f7db25ae2ee095dad42e2578b1e800 # B20_FACTORY
0x47a1afe8d3d691b87e090ee972d223a11f4da971ff5416c04985bb2393aca752 # B20_TOKEN
0xcdcc772fe4cbdb1029f822861176d09e646db96723d4c1e82ddfdeb8163ef54c # B20_ASSET
0xb582ebae03f16fee49a6763f78df482fb11ae73f103ed0d330bbe556aa90a43f # POLICY_REGISTRY
0xecfa0def2c10020caaf65e6155aa69c84b24892aaef76eeac52e0e2b3a0b8601 # B20_STABLECOIN (added base/base#2806)
0x83d32fab502ae0e8bc4352a117767262cb5e47cc8d67a744008ed4ff03fcf5e6 # B20_SECURITY (added base/base#2813)
0xecfa0def2c10020caaf65e6155aa69c84b24892aaef76eeac52e0e2b3a0b8601 # B20_STABLECOIN
)

# ── Helpers ───────────────────────────────────────────────────────────────────
Expand Down
4 changes: 2 additions & 2 deletions test/lib/mocks/MockActivationRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {MockActivationRegistryStorage} from "test/lib/mocks/MockActivationRegist
/// ERC-7201-namespaced root; see that library for the layout.
///
/// **Admin model.** `admin()` returns a hardcoded constant
/// (`0xCB00…0000` per base/base#2733) — the production
/// (`0xCB00…0000`) — the production
/// precompile and this mock both expose the same fixed admin
/// identity. There is no storage-backed admin slot and no
/// admin-rotation surface; replacing the admin requires a
Expand All @@ -39,7 +39,7 @@ contract MockActivationRegistry is IActivationRegistry {
// ============================================================

/// @notice The activation admin address. Hardcoded to the same value the
/// live Rust precompile returns (`0xCB00…0000`, per base/base#2733),
/// live Rust precompile returns (`0xCB00…0000`),
/// so tests and consumers can pin the admin identity without
/// depending on per-environment configuration.
address internal constant ADMIN = 0xCB00000000000000000000000000000000000000;
Expand Down
33 changes: 15 additions & 18 deletions test/lib/mocks/MockB20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -133,19 +133,7 @@ abstract contract MockB20 is IB20 {
/// / `setRoleAdmin`, where the gate is over the meta-role,
/// not the role itself.
modifier onlyRoleAdmin(bytes32 role) {
if (!_isPrivileged()) {
// Admin-resurrection guard: once `adminCount == 0` the token is admin-less
// by design (post-`renounceLastAdmin`). No role-mgmt mutation is permitted
// even if the caller holds a custom-admin role that would otherwise
// authorize them, because allowing such mutations would let a custom-admin
// chain (e.g. setRoleAdmin(MINT_ROLE, BURN_ROLE) → grantRole(BURN_ROLE, X))
// re-establish admin power on a token that has explicitly relinquished it.
// Mirrors base/base PR #2961 (`ensure_role_admin_mutations_available`).
if (MockB20Storage.layout().adminCount == 0) {
revert AccessControlUnauthorizedAccount(msg.sender, DEFAULT_ADMIN_ROLE);
}
_requireRoleAdmin(role);
}
if (!_isPrivileged()) _requireRoleAdmin(role);
_;
}

Expand Down Expand Up @@ -408,8 +396,7 @@ abstract contract MockB20 is IB20 {
function pausedFeatures() external view returns (PausableFeature[] memory) {
uint256 vectors = MockB20Storage.layout().pausedVectors;
uint256 count;
// Bound the scan off `type(PausableFeature).max` so it tracks the enum as variants are
// added or removed (the REDEEM variant was removed in BOP-251) rather than a stale literal.
// Derive the bound from the enum so it can't drift out of sync with PausableFeature.
uint256 featureCount = uint256(type(PausableFeature).max) + 1;
for (uint256 i = 0; i < featureCount; i++) {
if (((vectors >> i) & uint256(1)) == 1) count++;
Expand Down Expand Up @@ -496,16 +483,16 @@ abstract contract MockB20 is IB20 {
/// falling through to `super`.
function _writePolicyId(bytes32 policyScope, uint64 newPolicyId) internal virtual {
MockB20Storage.Layout storage $ = MockB20Storage.layout();
// `updatePolicy` validates the scope via `_readPolicyId` before calling this, so it is
// guaranteed to be one of the four supported scopes — the last arm needs no guard.
if (policyScope == TRANSFER_SENDER_POLICY) {
$.transferPolicyIds.sender = newPolicyId;
} else if (policyScope == TRANSFER_RECEIVER_POLICY) {
$.transferPolicyIds.receiver = newPolicyId;
} else if (policyScope == TRANSFER_EXECUTOR_POLICY) {
$.transferPolicyIds.executor = newPolicyId;
} else if (policyScope == MINT_RECEIVER_POLICY) {
$.mintPolicyIds.receiver = newPolicyId;
} else {
revert UnsupportedPolicyType(policyScope);
$.mintPolicyIds.receiver = newPolicyId;
}
}

Expand Down Expand Up @@ -655,6 +642,16 @@ abstract contract MockB20 is IB20 {
/// unconfigured roles it's `bytes32(0) == DEFAULT_ADMIN_ROLE`,
/// which is the role the caller actually needs to hold.
function _requireRoleAdmin(bytes32 role) internal view {
// Admin-resurrection guard: once `adminCount == 0` the token is admin-less
// by design (post-`renounceLastAdmin`). No role-mgmt mutation is permitted
// even if the caller holds a custom-admin role that would otherwise
// authorize them, because allowing such mutations would let a custom-admin
// chain (e.g. setRoleAdmin(MINT_ROLE, BURN_ROLE) → grantRole(BURN_ROLE, X))
// re-establish admin power on a token that has explicitly relinquished it.
// Mirrors the Rust precompile's `ensure_role_admin_mutations_available`.
if (MockB20Storage.layout().adminCount == 0) {
revert AccessControlUnauthorizedAccount(msg.sender, DEFAULT_ADMIN_ROLE);
}
bytes32 adminRole = MockB20Storage.layout().roleAdmins[role];
if (!hasRole(adminRole, msg.sender)) revert AccessControlUnauthorizedAccount(msg.sender, adminRole);
}
Expand Down
10 changes: 5 additions & 5 deletions test/lib/mocks/MockB20Factory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,6 @@ contract MockB20Factory is IB20Factory {
// any moment. Variant-feature mapping:
// STABLECOIN → B20_STABLECOIN
// ASSET → B20_ASSET
// (The dedicated `B20_TOKEN` feature was retired by BOP-257
// and the `DEFAULT` variant by BOP-253.)
_enforceActivationGates(variant);

// -- 1. Decode + validate, get the common params --
Expand Down Expand Up @@ -146,9 +144,11 @@ contract MockB20Factory is IB20Factory {
decimals = 6;
currency_ = p.currency;
} else {
// Unreachable in Solidity: an out-of-range `B20Variant` is rejected by ABI
// enum-decoding (Panic 0x21) before this body runs. Retained to mirror the Rust
// precompile, which decodes the raw discriminator and surfaces this typed revert.
// Unreachable in Solidity, and intentionally so: an out-of-range `B20Variant` is
// rejected by ABI enum-decoding (Panic 0x21) before this body runs, so no test can
// reach it — this is the single line the coverage report shows uncovered, by design.
// It is retained to mirror the Rust precompile, which decodes the raw discriminator
// and surfaces this typed `InvalidVariant` revert.
revert InvalidVariant();
}

Expand Down
2 changes: 1 addition & 1 deletion test/lib/mocks/MockPolicyRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ contract MockPolicyRegistry is IPolicyRegistry {
/// `updateAllowlist`, and `updateBlocklist` revert with
/// `BatchSizeTooLarge(MAX_BATCH_SIZE)` when `accounts.length`
/// exceeds this value. Mirrors the Rust PolicyRegistry
/// precompile (base/base#2876).
/// precompile.
uint256 internal constant MAX_BATCH_SIZE = 64;

// ============================================================
Expand Down
Loading
Loading