From 72f036709991844776d54d04493427ce11d32af5 Mon Sep 17 00:00:00 2001 From: Conner Swenberg Date: Tue, 2 Jun 2026 02:40:18 -0400 Subject: [PATCH 1/7] docs: correct stale batchBurn ref; trim changelog narration from a comment - docs/B20/Asset.md: announcements coupled "batched mints/burns", but batchBurn was removed (BOP-250); only batchMint remains. - MockB20.pausedFeatures: replace a comment that narrated a past change with a present-tense intent note (the bound is derived from the enum). Inline comments should explain why the code is shaped as it is now, not changelog it; the when/why of a change belongs in git history. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/B20/Asset.md | 2 +- test/lib/mocks/MockB20.sol | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/B20/Asset.md b/docs/B20/Asset.md index 22bd9db..c819999 100644 --- a/docs/B20/Asset.md +++ b/docs/B20/Asset.md @@ -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 diff --git a/test/lib/mocks/MockB20.sol b/test/lib/mocks/MockB20.sol index 25fe739..2eb8fb5 100644 --- a/test/lib/mocks/MockB20.sol +++ b/test/lib/mocks/MockB20.sol @@ -408,8 +408,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++; From 9af2558d57f722763c62ce0287c907fbeb057f04 Mon Sep 17 00:00:00 2001 From: Conner Swenberg Date: Tue, 2 Jun 2026 02:56:29 -0400 Subject: [PATCH 2/7] test(regression): standardize ticket tags in @dev; prune stray ticket refs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopt a consistent convention for the regression suite: each test declares the change it guards with a single trailing `Regression: BOP-XXX.` in its @dev. Remove ticket references from assert/revert messages, section dividers, and the file headers — those describe the assertion or the suite, not its provenance. Also prune the changelog parenthetical from MockB20Factory's activation-gate comment (the variant->feature mapping is present-tense intent; the "retired by BOP-257 / BOP-253" narration belongs in git history, not inline). Co-Authored-By: Claude Opus 4.8 (1M context) --- test/lib/mocks/MockB20Factory.sol | 2 - test/regression/B20Removals.t.sol | 87 +++++++++++++------------------ test/regression/B20Renames.t.sol | 86 ++++++++++++++---------------- 3 files changed, 75 insertions(+), 100 deletions(-) diff --git a/test/lib/mocks/MockB20Factory.sol b/test/lib/mocks/MockB20Factory.sol index 32cf295..d9fab31 100644 --- a/test/lib/mocks/MockB20Factory.sol +++ b/test/lib/mocks/MockB20Factory.sol @@ -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 -- diff --git a/test/regression/B20Removals.t.sol b/test/regression/B20Removals.t.sol index ff59b1a..110efd1 100644 --- a/test/regression/B20Removals.t.sol +++ b/test/regression/B20Removals.t.sol @@ -9,30 +9,21 @@ import {B20AssetTest} from "test/lib/B20AssetTest.sol"; /// @title B20 removal regression suite /// -/// @notice Locks in the *removals* from the B-20 asset rework (the -/// redemption subsystem, batched burn / burn-from, and the `DEFAULT` token variant). -/// Each test asserts that a function/selector that USED to exist -/// on the surface no longer resolves, so a future reintroduction -/// — or a Rust precompile in `base/base` that still exposes the -/// retired surface — fails here. +/// @notice Locks in the *removals* from the B-20 asset rework (the redemption subsystem, +/// batched burn / burn-from, and the `DEFAULT` token variant). Each test asserts that a +/// function/selector that USED to exist on the surface no longer resolves, so a future +/// reintroduction — or a Rust precompile in `base/base` that still exposes the retired +/// surface — fails here. /// -/// @dev These are *selector-absence* assertions: the token mock (and -/// the live precompile in fork mode) carries no fallback, so a -/// call to a removed selector cannot succeed. The same test body -/// is meaningful against the mock (proves the Solidity reference -/// dropped the surface) and against the live precompile under -/// `LIVE_PRECOMPILES=true ... --fork-url` (proves `base/base` -/// dropped it too). Args are irrelevant — dispatch fails before -/// decoding for an unknown selector — but old signatures are -/// reproduced verbatim so each `bytes4` matches the historical -/// selector exactly. +/// @dev These are *selector-absence* assertions: the token mock (and the live precompile in +/// fork mode) carries no fallback, so a call to a removed selector cannot succeed. The +/// same test body is meaningful against the mock (proves the Solidity reference dropped +/// the surface) and against the live precompile under `LIVE_PRECOMPILES=true ... --fork-url` +/// (proves `base/base` dropped it too). Args are irrelevant — dispatch fails before decoding +/// for an unknown selector — but old signatures are reproduced verbatim so each `bytes4` +/// matches the historical selector exactly. /// -/// Removal provenance: -/// - Redemption subsystem (`redeem`, `redeemWithMemo`, -/// `minimumRedeemable`, `updateMinimumRedeemable`, -/// `REDEEM_SENDER_POLICY`, the `REDEEM` pausable feature) — removed in BOP-251. -/// - `batchBurn` + `BURN_FROM_ROLE` — removed in BOP-250. -/// - `DEFAULT` B-20 variant — removed in BOP-253. +/// Each test tags the change it guards with a trailing `Regression: BOP-XXX.` line. contract B20RemovalsTest is B20AssetTest { /// @dev Asserts a low-level call of `signature` (with `args` appended) to the token reverts, /// i.e. the selector no longer resolves on the B-20 surface. @@ -46,56 +37,51 @@ contract B20RemovalsTest is B20AssetTest { // ============================================================ /// @notice Verifies the `redeem(uint256)` selector no longer resolves on the B-20 surface - /// @dev Redemption subsystem removed (BOP-251); a reintroduced `redeem` entrypoint trips this. + /// @dev A reintroduced `redeem` entrypoint trips this. Regression: BOP-251. function test_redeem_revert_selectorRemoved(uint256 amount) public { - _assertSelectorRemoved( - abi.encodeWithSignature("redeem(uint256)", amount), "redeem(uint256) must not resolve (removed BOP-251)" - ); + _assertSelectorRemoved(abi.encodeWithSignature("redeem(uint256)", amount), "redeem(uint256) must not resolve"); } /// @notice Verifies the `redeemWithMemo(uint256,bytes32)` selector no longer resolves - /// @dev Memo'd redemption removed alongside the redemption subsystem (BOP-251). + /// @dev Memo'd variant of the removed redeem path. Regression: BOP-251. function test_redeemWithMemo_revert_selectorRemoved(uint256 amount, bytes32 memo) public { _assertSelectorRemoved( abi.encodeWithSignature("redeemWithMemo(uint256,bytes32)", amount, memo), - "redeemWithMemo(uint256,bytes32) must not resolve (removed BOP-251)" + "redeemWithMemo(uint256,bytes32) must not resolve" ); } /// @notice Verifies the `minimumRedeemable()` getter no longer resolves - /// @dev The redemption floor parameter was removed with the redemption subsystem (BOP-251). + /// @dev The redemption floor getter was removed with the redemption subsystem. Regression: BOP-251. function test_minimumRedeemable_revert_selectorRemoved() public { - _assertSelectorRemoved( - abi.encodeWithSignature("minimumRedeemable()"), "minimumRedeemable() must not resolve (removed BOP-251)" - ); + _assertSelectorRemoved(abi.encodeWithSignature("minimumRedeemable()"), "minimumRedeemable() must not resolve"); } /// @notice Verifies the `updateMinimumRedeemable(uint256)` setter no longer resolves - /// @dev The redemption floor setter was removed with the redemption subsystem (BOP-251). + /// @dev The redemption floor setter was removed with the redemption subsystem. Regression: BOP-251. function test_updateMinimumRedeemable_revert_selectorRemoved(uint256 newMin) public { _assertSelectorRemoved( abi.encodeWithSignature("updateMinimumRedeemable(uint256)", newMin), - "updateMinimumRedeemable(uint256) must not resolve (removed BOP-251)" + "updateMinimumRedeemable(uint256) must not resolve" ); } /// @notice Verifies the `REDEEM_SENDER_POLICY()` policy-scope constant no longer resolves - /// @dev The redemption policy lane was removed with the redemption subsystem (BOP-251). + /// @dev The redemption policy lane was removed with the redemption subsystem. Regression: BOP-251. function test_redeemSenderPolicy_revert_selectorRemoved() public { _assertSelectorRemoved( - abi.encodeWithSignature("REDEEM_SENDER_POLICY()"), - "REDEEM_SENDER_POLICY() must not resolve (removed BOP-251)" + abi.encodeWithSignature("REDEEM_SENDER_POLICY()"), "REDEEM_SENDER_POLICY() must not resolve" ); } /// @notice Verifies the `PausableFeature` enum has no fourth (`REDEEM`) member /// @dev `isPaused(PausableFeature)` ABI-decodes its arg as the enum; an out-of-range value (3) /// reverts (Panic 0x21). Index 2 (`BURN`) still decodes, bracketing the enum at exactly - /// {TRANSFER, MINT, BURN}. The `REDEEM` feature was removed in BOP-251. + /// {TRANSFER, MINT, BURN}. Regression: BOP-251. function test_isPaused_revert_noRedeemFeature() public { _assertSelectorRemoved( abi.encodeWithSignature("isPaused(uint8)", uint8(3)), - "isPaused must reject enum index 3 (REDEEM removed BOP-251)" + "isPaused must reject enum index 3 (out of PausableFeature range)" ); (bool ok,) = address(token).call(abi.encodeWithSignature("isPaused(uint8)", uint8(2))); @@ -103,8 +89,8 @@ contract B20RemovalsTest is B20AssetTest { } /// @notice Verifies the all-features-paused bitmask covers exactly three features - /// @dev `ALL_FEATURES_PAUSED == 7` is `TRANSFER | MINT | BURN` (0b111); a fourth feature - /// (REDEEM) would push it to 15. Pins the feature count at the library source-of-truth. + /// @dev `ALL_FEATURES_PAUSED == 7` is `TRANSFER | MINT | BURN` (0b111); a fourth feature would + /// push it to 15. Pins the feature count at the library source-of-truth. Regression: BOP-251. function test_allFeaturesPaused_success_threeFeatureBitmask() public pure { assertEq(B20Constants.ALL_FEATURES_PAUSED, 7, "ALL_FEATURES_PAUSED must be TRANSFER|MINT|BURN (0b111)"); } @@ -114,23 +100,20 @@ contract B20RemovalsTest is B20AssetTest { // ============================================================ /// @notice Verifies the `batchBurn(address[],uint256[])` selector no longer resolves - /// @dev Batched burn was removed in BOP-250; only `batchMint` remains on the asset variant. + /// @dev Only `batchMint` remains on the asset variant. Regression: BOP-250. function test_batchBurn_revert_selectorRemoved(address account, uint256 amount) public { address[] memory accounts = _singletonAddresses(account); uint256[] memory amounts = _singletonUints(amount); _assertSelectorRemoved( abi.encodeWithSignature("batchBurn(address[],uint256[])", accounts, amounts), - "batchBurn(address[],uint256[]) must not resolve (removed BOP-250)" + "batchBurn(address[],uint256[]) must not resolve" ); } /// @notice Verifies the `BURN_FROM_ROLE()` role constant no longer resolves - /// @dev The burn-from role was removed with batched burn in BOP-250; `burnBlocked` is the - /// remaining seize path and is gated by `BURN_BLOCKED_ROLE`. + /// @dev `burnBlocked` (gated by `BURN_BLOCKED_ROLE`) is the remaining seize path. Regression: BOP-250. function test_burnFromRole_revert_selectorRemoved() public { - _assertSelectorRemoved( - abi.encodeWithSignature("BURN_FROM_ROLE()"), "BURN_FROM_ROLE() must not resolve (removed BOP-250)" - ); + _assertSelectorRemoved(abi.encodeWithSignature("BURN_FROM_ROLE()"), "BURN_FROM_ROLE() must not resolve"); } // ============================================================ @@ -138,13 +121,13 @@ contract B20RemovalsTest is B20AssetTest { // ============================================================ /// @notice Verifies the factory rejects a third (`DEFAULT`) variant discriminator - /// @dev `B20Variant` is now exactly {ASSET=0, STABLECOIN=1} (BOP-253 removed DEFAULT). - /// `createB20` ABI-decodes `variant` as the enum, so discriminator 2 reverts (Panic 0x21) - /// before reaching the factory body. A reintroduced third variant would let this succeed. + /// @dev `B20Variant` is now exactly {ASSET=0, STABLECOIN=1}. `createB20` ABI-decodes `variant` + /// as the enum, so discriminator 2 reverts (Panic 0x21) before reaching the factory body; + /// a reintroduced third variant would let this succeed. Regression: BOP-253. function test_createB20_revert_defaultVariantRemoved(bytes32 salt) public { bytes memory params = abi.encode(_assetParams()); (bool ok,) = address(factory) .call(abi.encodeWithSelector(IB20Factory.createB20.selector, uint8(2), salt, params, new bytes[](0))); - assertFalse(ok, "createB20 must reject variant discriminator 2 (DEFAULT removed BOP-253)"); + assertFalse(ok, "createB20 must reject variant discriminator 2 (out of B20Variant range)"); } } diff --git a/test/regression/B20Renames.t.sol b/test/regression/B20Renames.t.sol index f2b7e59..05e8c38 100644 --- a/test/regression/B20Renames.t.sol +++ b/test/regression/B20Renames.t.sol @@ -10,21 +10,19 @@ import {ActivationRegistryFeatureList} from "test/lib/mocks/ActivationRegistryFe /// @title B20 rename regression suite /// -/// @notice Locks in the *renames* from the B-20 asset rework: the -/// operator role (`SECURITY_OPERATOR_ROLE` → `OPERATOR_ROLE`, -/// BOP-248), share-ratio scaling (`sharesToTokensRatio`/`toShares`/ -/// `sharesOf`/`updateShareRatio` → `multiplier`/`toScaledBalance`/ -/// `scaledBalanceOf`/`updateMultiplier`, BOP-249), and the -/// activation feature namespace (`base.b20_security` → `base.b20_asset`, -/// `base.b20_token` removed, BOP-257). Each test asserts the new -/// surface is present and correct AND the old surface is gone, so the -/// rename cannot silently regress in either the Solidity reference or -/// the `base/base` Rust precompile (under fork mode). +/// @notice Locks in the *renames* from the B-20 asset rework: the operator role +/// (`SECURITY_OPERATOR_ROLE` → `OPERATOR_ROLE`), share-ratio scaling +/// (`sharesToTokensRatio`/`toShares`/`sharesOf`/`updateShareRatio` → `multiplier`/ +/// `toScaledBalance`/`scaledBalanceOf`/`updateMultiplier`), the METADATA/OPERATOR +/// authority split, and the `base.b20_asset` activation namespace. Each test asserts the +/// new surface is present and correct AND the old surface is gone, so the rename cannot +/// silently regress in either the Solidity reference or the `base/base` Rust precompile +/// (under fork mode). /// -/// @dev Old-selector absence is checked with low-level calls (the token -/// carries no fallback, so a retired selector cannot resolve). New -/// surface is checked with typed calls that only compile against the -/// current interface. +/// @dev Old-selector absence is checked with low-level calls (the token carries no fallback, so +/// a retired selector cannot resolve); new surface is checked with typed calls that only +/// compile against the current interface. Each test tags the change it guards with a +/// trailing `Regression: BOP-XXX.` line. contract B20RenamesTest is B20AssetTest { /// @dev Asserts a removed selector no longer resolves on the token surface. function _assertSelectorRemoved(bytes memory callData, string memory err) internal { @@ -33,31 +31,30 @@ contract B20RenamesTest is B20AssetTest { } // ============================================================ - // OPERATOR ROLE (BOP-248 rename) + // OPERATOR ROLE // ============================================================ /// @notice Verifies the operator role is exposed as `OPERATOR_ROLE` and the legacy /// `SECURITY_OPERATOR_ROLE` selector is gone - /// @dev BOP-248 renamed `SECURITY_OPERATOR_ROLE` → `OPERATOR_ROLE`; the wire value - /// (`keccak256("OPERATOR_ROLE")`) and library source-of-truth must agree, and the old - /// getter must not resolve. + /// @dev The wire value (`keccak256("OPERATOR_ROLE")`) and library source-of-truth must agree, + /// and the old getter must not resolve. Regression: BOP-248. function test_operatorRole_success_renamedFromSecurityOperator() public { assertEq(asset().OPERATOR_ROLE(), keccak256("OPERATOR_ROLE"), "OPERATOR_ROLE must equal its keccak preimage"); assertEq(asset().OPERATOR_ROLE(), B20Constants.OPERATOR_ROLE, "OPERATOR_ROLE must match B20Constants"); _assertSelectorRemoved( abi.encodeWithSignature("SECURITY_OPERATOR_ROLE()"), - "SECURITY_OPERATOR_ROLE() must not resolve (renamed to OPERATOR_ROLE in BOP-248)" + "SECURITY_OPERATOR_ROLE() must not resolve (renamed to OPERATOR_ROLE)" ); } // ============================================================ - // MULTIPLIER (BOP-249 rename) + // MULTIPLIER // ============================================================ /// @notice Verifies share-ratio scaling is exposed under the `multiplier` names and the legacy /// share-ratio selectors are gone - /// @dev BOP-249 renamed the share-ratio surface to multiplier scaling. The new getters resolve - /// (a fresh token reports a WAD multiplier), and every legacy selector must not resolve. + /// @dev The new getters resolve (a fresh token reports a WAD multiplier) and every legacy + /// selector must not resolve. Regression: BOP-249. function test_multiplier_success_renamedFromShareRatio(uint256 rawBalance) public { rawBalance = bound(rawBalance, 0, type(uint128).max); @@ -69,34 +66,33 @@ contract B20RenamesTest is B20AssetTest { // Legacy share-ratio surface is gone. _assertSelectorRemoved( abi.encodeWithSignature("sharesToTokensRatio()"), - "sharesToTokensRatio() must not resolve (renamed to multiplier() in BOP-249)" + "sharesToTokensRatio() must not resolve (renamed to multiplier())" ); _assertSelectorRemoved( abi.encodeWithSignature("toShares(uint256)", rawBalance), - "toShares(uint256) must not resolve (renamed to toScaledBalance in BOP-249)" + "toShares(uint256) must not resolve (renamed to toScaledBalance)" ); _assertSelectorRemoved( abi.encodeWithSignature("sharesOf(address)", alice), - "sharesOf(address) must not resolve (renamed to scaledBalanceOf in BOP-249)" + "sharesOf(address) must not resolve (renamed to scaledBalanceOf)" ); _assertSelectorRemoved( abi.encodeWithSignature("updateShareRatio(uint256)", rawBalance), - "updateShareRatio(uint256) must not resolve (renamed to updateMultiplier in BOP-249)" + "updateShareRatio(uint256) must not resolve (renamed to updateMultiplier)" ); } // ============================================================ - // METADATA vs OPERATOR GATING (BOP-248) + // METADATA vs OPERATOR GATING // ============================================================ - // BOP-248 split the asset variant's authority: the metadata - // setters (updateName / updateSymbol / updateContractURI / - // updateExtraMetadata) are gated by METADATA_ROLE, while the - // operator actions (announce / updateMultiplier) are gated by - // OPERATOR_ROLE. The two tests below pin that split from both sides. + // The asset variant splits authority: the metadata setters (updateName / updateSymbol / + // updateContractURI / updateExtraMetadata) are gated by METADATA_ROLE, while the operator + // actions (announce / updateMultiplier) are gated by OPERATOR_ROLE. The tests below pin that + // split from both sides. /// @notice Verifies `updateExtraMetadata` is gated by METADATA_ROLE, not OPERATOR_ROLE /// @dev An OPERATOR_ROLE-only holder is rejected with the METADATA_ROLE selector; a - /// METADATA_ROLE holder succeeds. Locks the BOP-248 gating split for metadata writes. + /// METADATA_ROLE holder succeeds. Regression: BOP-248. function test_updateExtraMetadata_success_gatedByMetadataRole(string calldata value) public { // Operator (OPERATOR_ROLE only) cannot write metadata. _grantOperator(); @@ -115,7 +111,7 @@ contract B20RenamesTest is B20AssetTest { /// @notice Verifies `updateMultiplier` is gated by OPERATOR_ROLE, not METADATA_ROLE /// @dev A METADATA_ROLE-only holder is rejected with the OPERATOR_ROLE selector — the inverse - /// of the metadata-gating test, confirming the two authorities are genuinely distinct. + /// of the metadata-gating test, confirming the two authorities are distinct. Regression: BOP-248. function test_updateMultiplier_revert_metadataRoleInsufficient(uint256 newMultiplier) public { newMultiplier = bound(newMultiplier, 1, type(uint128).max); _grantRole(B20Constants.METADATA_ROLE, bob); @@ -127,10 +123,9 @@ contract B20RenamesTest is B20AssetTest { } /// @notice Verifies METADATA_ROLE is administered by DEFAULT_ADMIN_ROLE on a freshly created token - /// @dev Pins the role-admin wiring: the asset variant does not set a custom admin for METADATA_ROLE, - /// so it defaults to DEFAULT_ADMIN_ROLE (the default admin grants/revokes METADATA_ROLE). - /// Authority over metadata *operations* is separate and split per the two tests above - /// (writes need METADATA_ROLE; operator actions need OPERATOR_ROLE). + /// @dev The asset variant does not set a custom admin for METADATA_ROLE, so it defaults to + /// DEFAULT_ADMIN_ROLE (the default admin grants/revokes METADATA_ROLE). Authority over + /// metadata *operations* is separate and split per the two tests above. Regression: BOP-248. function test_metadataRole_success_administeredByDefaultAdmin() public view { assertEq( token.getRoleAdmin(B20Constants.METADATA_ROLE), @@ -140,12 +135,12 @@ contract B20RenamesTest is B20AssetTest { } // ============================================================ - // ACTIVATION FEATURE NAMESPACE (BOP-257 rename) + // ACTIVATION FEATURE NAMESPACE // ============================================================ /// @notice Verifies the asset activation feature is keyed on the `base.b20_asset` namespace - /// @dev BOP-257 renamed `base.b20_security` → `base.b20_asset`. This is the cross-language - /// contract with the Rust `ActivationFeature` enum; a preimage drift desyncs the gate. + /// @dev This is the cross-language contract with the Rust `ActivationFeature` enum; a preimage + /// drift desyncs the gate. Regression: BOP-257. function test_b20Asset_success_keyedOnAssetNamespace() public pure { assertEq( ActivationRegistryFeatureList.B20_ASSET, @@ -155,17 +150,16 @@ contract B20RenamesTest is B20AssetTest { } /// @notice Verifies the asset feature is not keyed on either retired namespace - /// @dev BOP-257 renamed `base.b20_security` and removed `base.b20_token`. Asserting the asset - /// id differs from both retired preimages locks against an accidental revert to the old - /// namespace string. + /// @dev Asserting the asset id differs from both retired preimages locks against an accidental + /// revert to the old `base.b20_security` / `base.b20_token` namespace string. Regression: BOP-257. function test_b20Asset_success_notKeyedOnLegacyNamespaces() public pure { assertTrue( ActivationRegistryFeatureList.B20_ASSET != keccak256("base.b20_security"), - "B20_ASSET must not use the retired base.b20_security namespace (renamed BOP-257)" + "B20_ASSET must not use the retired base.b20_security namespace" ); assertTrue( ActivationRegistryFeatureList.B20_ASSET != keccak256("base.b20_token"), - "B20_ASSET must not use the retired base.b20_token namespace (removed BOP-257)" + "B20_ASSET must not use the retired base.b20_token namespace" ); } } From 18ae8215899b65cb226c223d8a113052731d2263 Mon Sep 17 00:00:00 2001 From: Conner Swenberg Date: Tue, 2 Jun 2026 03:02:49 -0400 Subject: [PATCH 3/7] =?UTF-8?q?test:=20sweep=20ticket=20refs=20repo-wide?= =?UTF-8?q?=20=E2=80=94=20formalize=20regressions,=20prune=20the=20rest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the ticket-reference policy across the whole suite (not just the new regression files): - Internal BOP refs that name a change a test exists to guard are formalized to the trailing `Regression: BOP-XXX.` convention (revokeRole last-admin guard, createPolicyWithAccounts check-order, createB20 stablecoin-decimals sanity). - Incidental changelog narration and internal PR refs are removed ("introduced in BOP-...", "post-BOP-196", "see PR #89", "PR #91 ... before that fix", "BOP-176 Rust-side", "BOP-207 tracks the reorder", "post-#40"). - External upstream provenance is kept (it pins where the mirrored Rust spec lives) and normalized to the consistent `base/base#NNNN` form (was "base/base PR #2961" in two places). Net: internal ticket numbers appear in exactly one predictable place — a test's `@dev Regression: BOP-XXX.` tag — and nowhere else. Co-Authored-By: Claude Opus 4.8 (1M context) --- script/__pycache__/mutate.cpython-312.pyc | Bin 0 -> 25805 bytes script/mutate.py | 2 +- test/lib/mocks/MockB20.sol | 2 +- test/unit/B20/roles/grantRole.t.sol | 2 +- test/unit/B20/roles/revokeRole.t.sol | 6 +++--- .../unit/B20/roles/revokeRole_revertOrder.t.sol | 7 +++---- .../B20Asset/batch/batchMint_rollback.t.sol | 2 +- .../B20Factory/createB20_asset_decimals.t.sol | 5 ++--- .../unit/B20Factory/createB20_revertOrder.t.sol | 2 +- .../createPolicyWithAccounts.t.sol | 5 ++--- .../createPolicyWithAccounts_rollback.t.sol | 7 +++---- 11 files changed, 18 insertions(+), 22 deletions(-) create mode 100644 script/__pycache__/mutate.cpython-312.pyc diff --git a/script/__pycache__/mutate.cpython-312.pyc b/script/__pycache__/mutate.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ab3e30ce55d3d096fe19d7705fc736cb0d9baccb GIT binary patch literal 25805 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 literal 0 HcmV?d00001 diff --git a/script/mutate.py b/script/mutate.py index 864eb50..f7aac80 100644 --- a/script/mutate.py +++ b/script/mutate.py @@ -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) {", diff --git a/test/lib/mocks/MockB20.sol b/test/lib/mocks/MockB20.sol index 2eb8fb5..1c40217 100644 --- a/test/lib/mocks/MockB20.sol +++ b/test/lib/mocks/MockB20.sol @@ -140,7 +140,7 @@ abstract contract MockB20 is IB20 { // 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`). + // Mirrors base/base#2961 (`ensure_role_admin_mutations_available`). if (MockB20Storage.layout().adminCount == 0) { revert AccessControlUnauthorizedAccount(msg.sender, DEFAULT_ADMIN_ROLE); } diff --git a/test/unit/B20/roles/grantRole.t.sol b/test/unit/B20/roles/grantRole.t.sol index 161453c..66fa302 100644 --- a/test/unit/B20/roles/grantRole.t.sol +++ b/test/unit/B20/roles/grantRole.t.sol @@ -67,7 +67,7 @@ contract B20GrantRoleTest is B20Test { /// @notice Verifies grantRole reverts on an admin-less token even when the caller holds a /// custom-admin role that would otherwise authorize the grant. - /// @dev Admin-role-resurrection guard — mirrors base/base PR #2961 + /// @dev Admin-role-resurrection guard — mirrors base/base#2961 /// (`ensure_role_admin_mutations_available`). Without this guard a custom-admin /// role chain set up while an admin existed (e.g. setRoleAdmin(MINT_ROLE, BURN_ROLE) /// + grantRole(BURN_ROLE, alice)) survives `renounceLastAdmin()` and lets BURN_ROLE diff --git a/test/unit/B20/roles/revokeRole.t.sol b/test/unit/B20/roles/revokeRole.t.sol index 086b643..8e161a4 100644 --- a/test/unit/B20/roles/revokeRole.t.sol +++ b/test/unit/B20/roles/revokeRole.t.sol @@ -24,13 +24,13 @@ contract B20RevokeRoleTest is B20Test { } /// @notice Verifies revokeRole reverts when the call would remove the sole DEFAULT_ADMIN_ROLE holder - /// @dev BOP-196 regression. Without this guard, `revokeRole(DEFAULT_ADMIN_ROLE, soleAdmin)` would + /// @dev Without this guard, `revokeRole(DEFAULT_ADMIN_ROLE, soleAdmin)` would /// succeed silently, bricking the token: admin operations become unreachable, and no /// `LastAdminRenounced` event is emitted to signal the terminal state. The dedicated /// `renounceLastAdmin()` path remains the only legitimate way to remove the final admin. /// Reverts with `LastAdminCannotRenounce` (reused from the renounceRole guard — same /// invariant: the last admin can only be removed via the explicit - /// `renounceLastAdmin` path). + /// `renounceLastAdmin` path). Regression: BOP-196. function test_revokeRole_revert_lastAdmin() public { assertEq(token.hasRole(B20Constants.DEFAULT_ADMIN_ROLE, admin), true, "precondition: admin is the sole admin"); @@ -42,7 +42,7 @@ contract B20RevokeRoleTest is B20Test { /// @notice Verifies revokeRole sets hasRole(role, account) to false /// @dev Read-after-write; canonical hasRole readback test lives in hasRole.t.sol. /// Skips revoking DEFAULT_ADMIN_ROLE from the sole bootstrap admin - /// (would revert with LastAdminCannotRenounce per the BOP-196 guard; + /// (would revert with LastAdminCannotRenounce per the last-admin guard; /// see test_revokeRole_revert_lastAdmin and renounceLastAdmin.t.sol /// for the terminal-admin removal path). /// Paired slot assertion: the `roles[role][account]` slot diff --git a/test/unit/B20/roles/revokeRole_revertOrder.t.sol b/test/unit/B20/roles/revokeRole_revertOrder.t.sol index 335922c..4fbe6bb 100644 --- a/test/unit/B20/roles/revokeRole_revertOrder.t.sol +++ b/test/unit/B20/roles/revokeRole_revertOrder.t.sol @@ -8,14 +8,13 @@ import {MockB20, B20Constants} from "test/lib/mocks/MockB20.sol"; /// @title Differential check-order tests for `revokeRole`. /// -/// @notice **Canonical order (Solidity reference, post-BOP-196):** +/// @notice **Canonical order (Solidity reference):** /// 1. ROLE (`onlyRoleAdmin(role)` modifier) → `AccessControlUnauthorizedAccount` /// 2. LAST-ADMIN (`role == DEFAULT_ADMIN && account holds it && adminCount == 1`) /// → `LastAdminCannotRenounce` /// -/// C(2, 2) = 1 pair. The LAST-ADMIN guard was added in -/// [PR #91 (BOP-196)](https://github.com/base/base-std/pull/91); before that fix -/// `revokeRole` would silently brick the token by removing the sole admin. +/// C(2, 2) = 1 pair. Without the LAST-ADMIN guard, `revokeRole` would silently +/// brick the token by removing the sole admin. contract B20RevokeRoleRevertOrderTest is B20Test { /// @notice ROLE beats LAST-ADMIN. /// @dev Caller lacks the role-admin role AND the target is the sole DEFAULT_ADMIN_ROLE holder. diff --git a/test/unit/B20Asset/batch/batchMint_rollback.t.sol b/test/unit/B20Asset/batch/batchMint_rollback.t.sol index 3a5285f..bf8996d 100644 --- a/test/unit/B20Asset/batch/batchMint_rollback.t.sol +++ b/test/unit/B20Asset/batch/batchMint_rollback.t.sol @@ -21,7 +21,7 @@ import {B20Constants} from "src/lib/B20Constants.sol"; /// signal is in fork mode against the real Rust precompile, /// where any break in the `precompile-storage` journal hookup /// would leave the earlier writes persisted across the revert. -/// Companion to BOP-176 (Rust-side unit tests). +/// Companion to the Rust-side unit tests. contract B20AssetBatchMintRollbackTest is B20AssetTest { /// @notice batchMint that reverts on element 1 via supply-cap leaves balances + supply unchanged /// @dev Element 0 mints under the cap (writes alice's balance and diff --git a/test/unit/B20Factory/createB20_asset_decimals.t.sol b/test/unit/B20Factory/createB20_asset_decimals.t.sol index 7fd3955..f7edc73 100644 --- a/test/unit/B20Factory/createB20_asset_decimals.t.sol +++ b/test/unit/B20Factory/createB20_asset_decimals.t.sol @@ -11,8 +11,7 @@ import {MockB20} from "test/lib/mocks/MockB20.sol"; import {MockB20AssetStorage} from "test/lib/mocks/MockB20Storage.sol"; import {B20FactoryTest} from "test/lib/B20FactoryTest.sol"; -/// @notice Coverage for the asset variant's configurable `decimals` field -/// introduced in BOP-252 / BOP-255 (storage) and BOP-259 (tests). +/// @notice Coverage for the asset variant's configurable `decimals` field. /// /// @dev Asserts the boundary values, the out-of-range revert with the new /// `InvalidDecimals(uint8)` error, fuzz coverage over the full @@ -213,7 +212,7 @@ contract B20FactoryCreateB20AssetDecimalsTest is B20FactoryTest { //////////////////////////////////////////////////////////////*/ /// @notice Verifies the stablecoin variant still hardcodes `decimals() == 6`. - /// BOP-255 is an asset-only change; the stablecoin path must be untouched. + /// @dev Configurable decimals is an asset-only change; the stablecoin path must be untouched. Regression: BOP-255. function test_createB20_success_stablecoin_decimalsStillHardcoded(address caller, bytes32 salt) public { _assumeValidCaller(caller); address token = _createStablecoin(caller, salt, _stablecoinParams(), new bytes[](0)); diff --git a/test/unit/B20Factory/createB20_revertOrder.t.sol b/test/unit/B20Factory/createB20_revertOrder.t.sol index b88070f..0adeab8 100644 --- a/test/unit/B20Factory/createB20_revertOrder.t.sol +++ b/test/unit/B20Factory/createB20_revertOrder.t.sol @@ -12,7 +12,7 @@ import {B20FactoryTest} from "test/lib/B20FactoryTest.sol"; /// ordering: VERSION beats each field-validation check inside an arm. /// The cross-arm UNSUPPORTED-VARIANT-beats-arm-body ordering is pinned by /// `test_createB20_revert_outOfRangeVariant` in `getTokenAddress.t.sol` -/// (raw-bytes ABI-decoder panic; see audit-response PR #89). +/// (raw-bytes ABI-decoder panic). /// /// **Canonical order per variant arm (Solidity reference):** /// - STABLECOIN: VERSION → INVALID-CURRENCY (format check on each byte) diff --git a/test/unit/PolicyRegistry/createPolicyWithAccounts.t.sol b/test/unit/PolicyRegistry/createPolicyWithAccounts.t.sol index 4738d7a..2f38a3e 100644 --- a/test/unit/PolicyRegistry/createPolicyWithAccounts.t.sol +++ b/test/unit/PolicyRegistry/createPolicyWithAccounts.t.sol @@ -156,9 +156,8 @@ contract PolicyRegistryCreatePolicyWithAccountsTest is PolicyRegistryTest { /// @dev Pins the Rust precompile's check precedence (`validate_create_policy_inputs` /// → `require_account_batch_size`). Mirrors Rust test /// `create_policy_with_accounts_zero_admin_precedes_batch_size_revert` in - /// `crates/common/precompiles/src/policy/storage.rs`. Regression guard for the - /// BOP-207 hoist: if a later refactor reorders these two entry-point checks, - /// this test fails. + /// `crates/common/precompiles/src/policy/storage.rs`. Guards the entry-point check + /// order: if a later refactor reorders these two checks, this test fails. Regression: BOP-207. function test_createPolicyWithAccounts_revert_zeroAdmin_precedes_batchSizeTooLarge( address caller, uint8 typeIdx, diff --git a/test/unit/PolicyRegistry/createPolicyWithAccounts_rollback.t.sol b/test/unit/PolicyRegistry/createPolicyWithAccounts_rollback.t.sol index 7279777..5249628 100644 --- a/test/unit/PolicyRegistry/createPolicyWithAccounts_rollback.t.sol +++ b/test/unit/PolicyRegistry/createPolicyWithAccounts_rollback.t.sol @@ -11,8 +11,8 @@ import {MockPolicyRegistryStorage} from "test/lib/mocks/MockPolicyRegistryStorag /// registry state. /// /// @dev Sibling of `PolicyRegistryCreatePolicyWithAccountsTest` (happy -/// path + revert reasons). Distinct from BOP-176 (Rust-side unit -/// tests of `precompile-storage`): this exercises the precompile +/// path + revert reasons). Distinct from the Rust-side unit +/// tests of `precompile-storage`: this exercises the precompile /// in a real EVM execution context to verify the JournaledState /// integration rolls back any partial writes — or, in the case /// of an impl that validates before creating, that no writes @@ -22,8 +22,7 @@ import {MockPolicyRegistryStorage} from "test/lib/mocks/MockPolicyRegistryStorag /// different paths: /// - The Solidity mock advances `nextCounter` and writes the /// policy slot before checking the batch size, then relies on -/// revm's journal to roll back on revert (BOP-207 tracks the -/// reorder). +/// revm's journal to roll back on revert. /// - The Rust precompile validates batch size first and never /// writes. /// End state is identical in both: counter unchanged, predicted From f49e5a2d77be1c707985be86b41cb2452586beb3 Mon Sep 17 00:00:00 2001 From: Conner Swenberg Date: Tue, 2 Jun 2026 03:09:24 -0400 Subject: [PATCH 4/7] test/docs: remove all PR/ticket-number refs outside the regression suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the agreed policy, ticket/PR numbers are a smell everywhere except the regression suite. Strip every remaining numeric reference from src/test/script comments and docs — internal BOP refs (including the Regression tags briefly added to non-regression unit tests) and external base/base#NNNN refs alike. Named/descriptive provenance is preserved where it carries real meaning: the Rust function name (`ensure_role_admin_mutations_available`), file paths (`crates/common/precompiles/...`), "the Rust precompile", and commit SHAs in the fork-testing troubleshooting notes stay; only the issue/PR numbers go. Ticket numbers now live in exactly one place: each regression test's `@dev Regression: BOP-XXX.` tag. Co-Authored-By: Claude Opus 4.8 (1M context) --- FORK_TESTING.md | 8 ++++---- script/run-fork-tests.sh | 6 +++--- test/lib/mocks/MockActivationRegistry.sol | 4 ++-- test/lib/mocks/MockB20.sol | 2 +- test/lib/mocks/MockPolicyRegistry.sol | 2 +- test/unit/ActivationRegistry/admin.t.sol | 2 +- test/unit/B20/roles/grantRole.t.sol | 4 ++-- test/unit/B20/roles/revokeRole.t.sol | 2 +- test/unit/B20Factory/createB20_asset_decimals.t.sol | 2 +- test/unit/PolicyRegistry/createPolicyWithAccounts.t.sol | 4 ++-- test/unit/PolicyRegistry/updateAllowlist.t.sol | 2 +- test/unit/PolicyRegistry/updateBlocklist.t.sol | 2 +- 12 files changed, 20 insertions(+), 20 deletions(-) diff --git a/FORK_TESTING.md b/FORK_TESTING.md index aafe93a..45c8f54 100644 --- a/FORK_TESTING.md +++ b/FORK_TESTING.md @@ -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 @@ -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. @@ -249,7 +249,7 @@ 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 | @@ -257,7 +257,7 @@ If this returns garbage / fails, the fork's build is broken or out of date. | 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 diff --git a/script/run-fork-tests.sh b/script/run-fork-tests.sh index 8403f86..420bb05 100755 --- a/script/run-fork-tests.sh +++ b/script/run-fork-tests.sh @@ -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: @@ -82,8 +82,8 @@ FEATURE_IDS=( 0x78751e29c8bcc0d609ab18e9fbc4158e73f7db25ae2ee095dad42e2578b1e800 # B20_FACTORY 0x47a1afe8d3d691b87e090ee972d223a11f4da971ff5416c04985bb2393aca752 # B20_TOKEN 0xb582ebae03f16fee49a6763f78df482fb11ae73f103ed0d330bbe556aa90a43f # POLICY_REGISTRY - 0xecfa0def2c10020caaf65e6155aa69c84b24892aaef76eeac52e0e2b3a0b8601 # B20_STABLECOIN (added base/base#2806) - 0x83d32fab502ae0e8bc4352a117767262cb5e47cc8d67a744008ed4ff03fcf5e6 # B20_SECURITY (added base/base#2813) + 0xecfa0def2c10020caaf65e6155aa69c84b24892aaef76eeac52e0e2b3a0b8601 # B20_STABLECOIN + 0x83d32fab502ae0e8bc4352a117767262cb5e47cc8d67a744008ed4ff03fcf5e6 # B20_SECURITY ) # ── Helpers ─────────────────────────────────────────────────────────────────── diff --git a/test/lib/mocks/MockActivationRegistry.sol b/test/lib/mocks/MockActivationRegistry.sol index 500a6b8..a1bef15 100644 --- a/test/lib/mocks/MockActivationRegistry.sol +++ b/test/lib/mocks/MockActivationRegistry.sol @@ -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 @@ -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; diff --git a/test/lib/mocks/MockB20.sol b/test/lib/mocks/MockB20.sol index 1c40217..e3de143 100644 --- a/test/lib/mocks/MockB20.sol +++ b/test/lib/mocks/MockB20.sol @@ -140,7 +140,7 @@ abstract contract MockB20 is IB20 { // 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#2961 (`ensure_role_admin_mutations_available`). + // Mirrors the Rust precompile's `ensure_role_admin_mutations_available`. if (MockB20Storage.layout().adminCount == 0) { revert AccessControlUnauthorizedAccount(msg.sender, DEFAULT_ADMIN_ROLE); } diff --git a/test/lib/mocks/MockPolicyRegistry.sol b/test/lib/mocks/MockPolicyRegistry.sol index 24c8249..bec7f49 100644 --- a/test/lib/mocks/MockPolicyRegistry.sol +++ b/test/lib/mocks/MockPolicyRegistry.sol @@ -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; // ============================================================ diff --git a/test/unit/ActivationRegistry/admin.t.sol b/test/unit/ActivationRegistry/admin.t.sol index f7dda50..66c2be4 100644 --- a/test/unit/ActivationRegistry/admin.t.sol +++ b/test/unit/ActivationRegistry/admin.t.sol @@ -7,7 +7,7 @@ contract ActivationRegistryAdminTest is ActivationRegistryTest { /// @notice Verifies admin returns the configured activation admin address /// @dev Constant readback; the mock-vs-live boundary returns the same address either way. /// The setUp-resolved `activationAdmin` is the same value the mock hardcodes - /// (`0xCB00…0000`, per base/base#2733), so a second readback must agree. + /// (`0xCB00…0000`), so a second readback must agree. function test_admin_success_returnsConfigured() public view { assertEq(activationRegistry.admin(), activationAdmin, "admin() must return the address resolved during setUp"); assertTrue(activationAdmin != address(0), "activation admin must be non-zero"); diff --git a/test/unit/B20/roles/grantRole.t.sol b/test/unit/B20/roles/grantRole.t.sol index 66fa302..acdfe17 100644 --- a/test/unit/B20/roles/grantRole.t.sol +++ b/test/unit/B20/roles/grantRole.t.sol @@ -67,8 +67,8 @@ contract B20GrantRoleTest is B20Test { /// @notice Verifies grantRole reverts on an admin-less token even when the caller holds a /// custom-admin role that would otherwise authorize the grant. - /// @dev Admin-role-resurrection guard — mirrors base/base#2961 - /// (`ensure_role_admin_mutations_available`). Without this guard a custom-admin + /// @dev Admin-role-resurrection guard — mirrors the Rust precompile's + /// `ensure_role_admin_mutations_available`. Without this guard a custom-admin /// role chain set up while an admin existed (e.g. setRoleAdmin(MINT_ROLE, BURN_ROLE) /// + grantRole(BURN_ROLE, alice)) survives `renounceLastAdmin()` and lets BURN_ROLE /// holders keep mutating the role graph on a token that has no admin. The shared diff --git a/test/unit/B20/roles/revokeRole.t.sol b/test/unit/B20/roles/revokeRole.t.sol index 8e161a4..98a7875 100644 --- a/test/unit/B20/roles/revokeRole.t.sol +++ b/test/unit/B20/roles/revokeRole.t.sol @@ -30,7 +30,7 @@ contract B20RevokeRoleTest is B20Test { /// `renounceLastAdmin()` path remains the only legitimate way to remove the final admin. /// Reverts with `LastAdminCannotRenounce` (reused from the renounceRole guard — same /// invariant: the last admin can only be removed via the explicit - /// `renounceLastAdmin` path). Regression: BOP-196. + /// `renounceLastAdmin` path). function test_revokeRole_revert_lastAdmin() public { assertEq(token.hasRole(B20Constants.DEFAULT_ADMIN_ROLE, admin), true, "precondition: admin is the sole admin"); diff --git a/test/unit/B20Factory/createB20_asset_decimals.t.sol b/test/unit/B20Factory/createB20_asset_decimals.t.sol index f7edc73..773ae06 100644 --- a/test/unit/B20Factory/createB20_asset_decimals.t.sol +++ b/test/unit/B20Factory/createB20_asset_decimals.t.sol @@ -212,7 +212,7 @@ contract B20FactoryCreateB20AssetDecimalsTest is B20FactoryTest { //////////////////////////////////////////////////////////////*/ /// @notice Verifies the stablecoin variant still hardcodes `decimals() == 6`. - /// @dev Configurable decimals is an asset-only change; the stablecoin path must be untouched. Regression: BOP-255. + /// @dev Configurable decimals is an asset-only change; the stablecoin path must be untouched. function test_createB20_success_stablecoin_decimalsStillHardcoded(address caller, bytes32 salt) public { _assumeValidCaller(caller); address token = _createStablecoin(caller, salt, _stablecoinParams(), new bytes[](0)); diff --git a/test/unit/PolicyRegistry/createPolicyWithAccounts.t.sol b/test/unit/PolicyRegistry/createPolicyWithAccounts.t.sol index 2f38a3e..d8aa7dc 100644 --- a/test/unit/PolicyRegistry/createPolicyWithAccounts.t.sol +++ b/test/unit/PolicyRegistry/createPolicyWithAccounts.t.sol @@ -121,7 +121,7 @@ contract PolicyRegistryCreatePolicyWithAccountsTest is PolicyRegistryTest { } /// @notice Verifies createPolicyWithAccounts reverts when the batch exceeds MAX_BATCH_SIZE - /// @dev Mirrors the Rust precompile's batch limit (base/base#2876); checks + /// @dev Mirrors the Rust precompile's batch limit; checks /// BatchSizeTooLarge(maxBatchSize). Fuzz drives `overflow` so the test exercises /// arbitrary over-the-limit sizes, not just the immediate neighbor. function test_createPolicyWithAccounts_revert_batchSizeTooLarge( @@ -157,7 +157,7 @@ contract PolicyRegistryCreatePolicyWithAccountsTest is PolicyRegistryTest { /// → `require_account_batch_size`). Mirrors Rust test /// `create_policy_with_accounts_zero_admin_precedes_batch_size_revert` in /// `crates/common/precompiles/src/policy/storage.rs`. Guards the entry-point check - /// order: if a later refactor reorders these two checks, this test fails. Regression: BOP-207. + /// order: if a later refactor reorders these two checks, this test fails. function test_createPolicyWithAccounts_revert_zeroAdmin_precedes_batchSizeTooLarge( address caller, uint8 typeIdx, diff --git a/test/unit/PolicyRegistry/updateAllowlist.t.sol b/test/unit/PolicyRegistry/updateAllowlist.t.sol index 43b955d..11a1cb3 100644 --- a/test/unit/PolicyRegistry/updateAllowlist.t.sol +++ b/test/unit/PolicyRegistry/updateAllowlist.t.sol @@ -129,7 +129,7 @@ contract PolicyRegistryUpdateAllowlistTest is PolicyRegistryTest { } /// @notice Verifies updateAllowlist reverts when the batch exceeds MAX_BATCH_SIZE - /// @dev Mirrors the Rust precompile's batch limit (base/base#2876); checks + /// @dev Mirrors the Rust precompile's batch limit; checks /// BatchSizeTooLarge(maxBatchSize). Fuzz drives `overflow` so the test exercises /// arbitrary over-the-limit sizes. function test_updateAllowlist_revert_batchSizeTooLarge(address currentAdmin, bool allowed, uint8 overflow) public { diff --git a/test/unit/PolicyRegistry/updateBlocklist.t.sol b/test/unit/PolicyRegistry/updateBlocklist.t.sol index b6d15cd..44fb275 100644 --- a/test/unit/PolicyRegistry/updateBlocklist.t.sol +++ b/test/unit/PolicyRegistry/updateBlocklist.t.sol @@ -129,7 +129,7 @@ contract PolicyRegistryUpdateBlocklistTest is PolicyRegistryTest { } /// @notice Verifies updateBlocklist reverts when the batch exceeds MAX_BATCH_SIZE - /// @dev Mirrors the Rust precompile's batch limit (base/base#2876); checks + /// @dev Mirrors the Rust precompile's batch limit; checks /// BatchSizeTooLarge(maxBatchSize). Fuzz drives `overflow` so the test exercises /// arbitrary over-the-limit sizes. function test_updateBlocklist_revert_batchSizeTooLarge(address currentAdmin, bool blocked, uint8 overflow) public { From a8856e8675be32bd1a7ac01caa632c9f2af1b9df Mon Sep 17 00:00:00 2001 From: Conner Swenberg Date: Tue, 2 Jun 2026 03:12:43 -0400 Subject: [PATCH 5/7] fix(fork-tests): align activation feature set with the Solidity reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit run-fork-tests.sh activated a stale feature set. Align it to the canonical Solidity reference (test/lib/mocks/ActivationRegistryFeatureList.sol) — the official source of truth — so the fork suite activates exactly the features the spec defines: - Drop the retired `B20_TOKEN` and `B20_FACTORY` ids (no longer feature flags). - Replace `B20_SECURITY` with `B20_ASSET` (`0xcdcc…`). - Repoint the comment at the Solidity reference instead of the Rust storage enum. The resulting set is identical to ActivationRegistryFeatureList. Against a caught-up node the asset feature (`base.b20_asset`) would otherwise never be activated, failing every asset-variant fork test on an inactive-feature gate. Co-Authored-By: Claude Opus 4.8 (1M context) --- script/run-fork-tests.sh | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/script/run-fork-tests.sh b/script/run-fork-tests.sh index 420bb05..a2c2dc4 100755 --- a/script/run-fork-tests.sh +++ b/script/run-fork-tests.sh @@ -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 - 0x83d32fab502ae0e8bc4352a117767262cb5e47cc8d67a744008ed4ff03fcf5e6 # B20_SECURITY ) # ── Helpers ─────────────────────────────────────────────────────────────────── From ee9f58a4fc2d4d6c199ab895cfe7280a02b9eadf Mon Sep 17 00:00:00 2001 From: Conner Swenberg Date: Tue, 2 Jun 2026 03:30:35 -0400 Subject: [PATCH 6/7] test(mock): close MockB20 coverage gaps (100% lines/branches/funcs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps surfaced once MockB20 became abstract (deployed only via MockB20Asset): - The onlyRoleAdmin admin-resurrection guard was inlined into the inherited grant/revoke/setRoleAdmin call sites, where forge couldn't attribute the branch back to MockB20.sol even though a test exercises it. Move the `adminCount == 0` guard into the `_requireRoleAdmin` internal function the modifier already calls — behavior-identical (the modifier still gates on `!_isPrivileged()`), and the check is now a normal internal-call site forge counts correctly. Also consolidates the admin-mutation gate in one place. - `_writePolicyId` carried an unreachable `else { revert UnsupportedPolicyType }`: its only caller (`updatePolicy`) already validates the scope via `_readPolicyId`, so by the write the scope is always one of the four supported. Collapse the dead arm. MockB20 now 100% lines/statements/branches/funcs; full suite still green. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/lib/mocks/MockB20.sol | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/test/lib/mocks/MockB20.sol b/test/lib/mocks/MockB20.sol index e3de143..a479881 100644 --- a/test/lib/mocks/MockB20.sol +++ b/test/lib/mocks/MockB20.sol @@ -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 the Rust precompile's `ensure_role_admin_mutations_available`. - if (MockB20Storage.layout().adminCount == 0) { - revert AccessControlUnauthorizedAccount(msg.sender, DEFAULT_ADMIN_ROLE); - } - _requireRoleAdmin(role); - } + if (!_isPrivileged()) _requireRoleAdmin(role); _; } @@ -495,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; } } @@ -654,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); } From 09cfa246023867eb95a32e33f73e7fc32453b656 Mon Sep 17 00:00:00 2001 From: Conner Swenberg Date: Tue, 2 Jun 2026 03:33:35 -0400 Subject: [PATCH 7/7] docs(mock): note the InvalidVariant branch is intentionally uncovered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `else { revert InvalidVariant(); }` in createB20 is unreachable in Solidity (ABI enum-decode panics on an out-of-range variant before the body runs), so it is the single line the coverage report shows uncovered — by design. Expand the inline comment to say so explicitly, and that it's retained to mirror the Rust precompile's typed revert. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/lib/mocks/MockB20Factory.sol | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/lib/mocks/MockB20Factory.sol b/test/lib/mocks/MockB20Factory.sol index d9fab31..2bfc55b 100644 --- a/test/lib/mocks/MockB20Factory.sol +++ b/test/lib/mocks/MockB20Factory.sol @@ -144,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(); }