From cb7194b7b69553d1b177150add03a806d4468ede Mon Sep 17 00:00:00 2001 From: Eric Shenghsiung Liu Date: Wed, 20 May 2026 10:06:18 -0400 Subject: [PATCH 1/8] Add CI workflow: build, test, fmt, coverage Runs on every push to main and every PR: - forge build --sizes (compilation + contract size report) - forge test -v (full test suite with fuzz) - forge fmt --check (formatting gate) - forge coverage --report lcov + Codecov upload (informational, not a gate) Codecov upload requires CODECOV_TOKEN secret to be set in repo settings. --- .github/workflows/ci.yml | 67 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..24fddb0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,67 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: foundry-rs/foundry-toolchain@v1 + + - name: Build + run: forge build --sizes + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: foundry-rs/foundry-toolchain@v1 + + - name: Run tests + run: forge test -v + + fmt: + name: Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: foundry-rs/foundry-toolchain@v1 + + - name: Check formatting + run: forge fmt --check + + coverage: + name: Coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: foundry-rs/foundry-toolchain@v1 + + - name: Generate coverage report + run: forge coverage --report lcov + + - name: Upload to Codecov + uses: codecov/codecov-action@v4 + with: + files: lcov.info + fail_ci_if_error: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From c1d3fa450deafe50fe45ca45dee0962777042b58 Mon Sep 17 00:00:00 2001 From: Eric Shenghsiung Liu Date: Wed, 27 May 2026 09:38:06 -0400 Subject: [PATCH 2/8] Fix CI: separate compilation from size check forge build --sizes exits non-zero when any contract exceeds EIP-170. MockTokenFactory is a test mock that intentionally exceeds the limit. Separate compilation (forge build) from the informational size report so test mocks do not block CI. --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24fddb0..43a61e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,10 @@ jobs: - uses: foundry-rs/foundry-toolchain@v1 - name: Build - run: forge build --sizes + run: forge build + + - name: Contract sizes (informational) + run: forge build --sizes || true test: name: Test From 2fb9739648bbe63fdf74d808be026eb4298ca100 Mon Sep 17 00:00:00 2001 From: Eric Shenghsiung Liu Date: Wed, 27 May 2026 09:39:04 -0400 Subject: [PATCH 3/8] Run forge fmt against rebased main --- src/interfaces/IB20.sol | 2 +- src/lib/B20FactoryLib.sol | 11 +-- test/lib/B20FactoryLibTest.sol | 5 +- test/lib/PolicyRegistryTest.sol | 3 +- test/lib/mocks/MockB20Factory.sol | 6 +- test/lib/mocks/MockB20Storage.sol | 23 +++++-- test/unit/B20/roles/renounceLastAdmin.t.sol | 13 +--- test/unit/B20/roles/renounceRole.t.sol | 4 +- test/unit/B20Factory/createToken.t.sol | 67 ++++++------------- test/unit/B20Factory/getTokenAddress.t.sol | 5 +- test/unit/B20FactoryLib/buildRoleGrants.t.sol | 8 +-- .../buildSecurityIdentifierUpdates.t.sol | 4 +- .../encodeDefaultCreateParams.t.sol | 5 +- .../encodeSecurityCreateParams.t.sol | 8 +-- .../encodeStablecoinCreateParams.t.sol | 3 +- .../B20FactoryLib/encodeUpdatePolicy.t.sol | 5 +- test/unit/PolicyRegistry/writeBuiltins.t.sol | 6 +- test/unit/storage/B20FullLayout.t.sol | 17 ++--- test/unit/storage/B20SecurityFullLayout.t.sol | 18 ++--- .../storage/PolicyRegistryFullLayout.t.sol | 8 +-- 20 files changed, 78 insertions(+), 143 deletions(-) diff --git a/src/interfaces/IB20.sol b/src/interfaces/IB20.sol index f8f2151..6901ecc 100644 --- a/src/interfaces/IB20.sol +++ b/src/interfaces/IB20.sol @@ -516,7 +516,7 @@ interface IB20 { /// @notice Updates the token's `name`. Requires `METADATA_ROLE`. /// No length restrictions. Emits `NameUpdated` followed by /// the ERC-5267 `EIP712DomainChanged()` event (in that - /// order). + /// order). /// @dev Several customers (Coinbase Tokenized Equities, Coinbase /// Wrapped Assets) need the ability to update name and symbol /// post-deployment for re-branding or legal-restructuring diff --git a/src/lib/B20FactoryLib.sol b/src/lib/B20FactoryLib.sol index fe7dee4..487be26 100644 --- a/src/lib/B20FactoryLib.sol +++ b/src/lib/B20FactoryLib.sol @@ -146,10 +146,7 @@ library B20FactoryLib { { return abi.encode( IB20Factory.B20CreateParams({ - version: B20_CREATE_PARAMS_VERSION, - name: name, - symbol: symbol, - initialAdmin: initialAdmin + version: B20_CREATE_PARAMS_VERSION, name: name, symbol: symbol, initialAdmin: initialAdmin }) ); } @@ -399,11 +396,7 @@ library B20FactoryLib { /// @param holders The security role-holder bundle. /// /// @return initCalls The ABI-encoded `grantRole` initCalls. - function buildRoleGrants(B20SecurityRoleHolders memory holders) - internal - pure - returns (bytes[] memory initCalls) - { + function buildRoleGrants(B20SecurityRoleHolders memory holders) internal pure returns (bytes[] memory initCalls) { bytes32[] memory roles = new bytes32[](8); roles[0] = B20Constants.MINT_ROLE; roles[1] = B20Constants.BURN_ROLE; diff --git a/test/lib/B20FactoryLibTest.sol b/test/lib/B20FactoryLibTest.sol index 30d6cff..b74d5c7 100644 --- a/test/lib/B20FactoryLibTest.sol +++ b/test/lib/B20FactoryLibTest.sol @@ -17,5 +17,6 @@ import {BaseTest} from "test/lib/BaseTest.sol"; /// Re-using those gives the test contracts a uniform vocabulary with /// the rest of the suite. No `setUp` extension is needed. contract B20FactoryLibTest is BaseTest { -// No additional state; `BaseTest`'s actor labels and helpers are sufficient. -} + // No additional state; `BaseTest`'s actor labels and helpers are sufficient. + + } diff --git a/test/lib/PolicyRegistryTest.sol b/test/lib/PolicyRegistryTest.sol index 598667d..c9be657 100644 --- a/test/lib/PolicyRegistryTest.sol +++ b/test/lib/PolicyRegistryTest.sol @@ -58,8 +58,7 @@ contract PolicyRegistryTest is BaseTest { /// sentinels before consuming it; the prediction matches by /// clamping pre-init reads up to `BUILTIN_POLICY_COUNT`. function _predictNextPolicyId(IPolicyRegistry.PolicyType policyType) internal view returns (uint64) { - uint56 counter = - uint56(uint256(vm.load(address(policyRegistry), MockPolicyRegistryStorage.nextCounterSlot()))); + uint56 counter = uint56(uint256(vm.load(address(policyRegistry), MockPolicyRegistryStorage.nextCounterSlot()))); if (counter < PolicyRegistryConstants.BUILTIN_POLICY_COUNT) { counter = PolicyRegistryConstants.BUILTIN_POLICY_COUNT; } diff --git a/test/lib/mocks/MockB20Factory.sol b/test/lib/mocks/MockB20Factory.sol index f180050..7cdec8f 100644 --- a/test/lib/mocks/MockB20Factory.sol +++ b/test/lib/mocks/MockB20Factory.sol @@ -313,11 +313,7 @@ contract MockB20Factory is IB20Factory { function _writeSecurityStorage(address token, string memory isin_, uint256 minimumRedeemable_) internal { _writeString(token, MockB20SecurityStorage.identifierSlot("ISIN"), isin_); _writeUint(token, MockB20RedeemStorage.minimumRedeemableSlot(), minimumRedeemable_); - _writeUint( - token, - MockB20RedeemStorage.redeemPolicyIdsSlot(), - uint256(PolicyRegistryConstants.ALWAYS_BLOCK_ID) - ); + _writeUint(token, MockB20RedeemStorage.redeemPolicyIdsSlot(), uint256(PolicyRegistryConstants.ALWAYS_BLOCK_ID)); } // ============================================================ diff --git a/test/lib/mocks/MockB20Storage.sol b/test/lib/mocks/MockB20Storage.sol index a3e91c0..ef27492 100644 --- a/test/lib/mocks/MockB20Storage.sol +++ b/test/lib/mocks/MockB20Storage.sol @@ -407,9 +407,17 @@ library MockB20SecurityStorage { // TOP-LEVEL FIELD SLOTS // ============================================================ - function sharesToTokensRatioSlot() internal pure returns (bytes32) { return slotOf(SHARES_TO_TOKENS_RATIO_OFFSET); } - function usedAnnouncementIdsBaseSlot() internal pure returns (bytes32) { return slotOf(USED_ANNOUNCEMENT_IDS_OFFSET); } - function identifiersBaseSlot() internal pure returns (bytes32) { return slotOf(IDENTIFIERS_OFFSET); } + function sharesToTokensRatioSlot() internal pure returns (bytes32) { + return slotOf(SHARES_TO_TOKENS_RATIO_OFFSET); + } + + function usedAnnouncementIdsBaseSlot() internal pure returns (bytes32) { + return slotOf(USED_ANNOUNCEMENT_IDS_OFFSET); + } + + function identifiersBaseSlot() internal pure returns (bytes32) { + return slotOf(IDENTIFIERS_OFFSET); + } // ============================================================ // MAPPING MEMBER SLOTS @@ -493,8 +501,13 @@ library MockB20RedeemStorage { // TOP-LEVEL FIELD SLOTS // ============================================================ - function minimumRedeemableSlot() internal pure returns (bytes32) { return slotOf(MINIMUM_REDEEMABLE_OFFSET); } - function redeemPolicyIdsSlot() internal pure returns (bytes32) { return slotOf(REDEEM_POLICY_IDS_OFFSET); } + function minimumRedeemableSlot() internal pure returns (bytes32) { + return slotOf(MINIMUM_REDEEMABLE_OFFSET); + } + + function redeemPolicyIdsSlot() internal pure returns (bytes32) { + return slotOf(REDEEM_POLICY_IDS_OFFSET); + } } /// @title MockB20StablecoinStorage diff --git a/test/unit/B20/roles/renounceLastAdmin.t.sol b/test/unit/B20/roles/renounceLastAdmin.t.sol index c19e979..1c1698a 100644 --- a/test/unit/B20/roles/renounceLastAdmin.t.sol +++ b/test/unit/B20/roles/renounceLastAdmin.t.sol @@ -129,21 +129,14 @@ contract B20RenounceLastAdminTest is B20Test { /// here too. function test_renounceLastAdmin_success_adminCountDrivenToZero() public { bytes32 adminCountSlot = MockB20Storage.adminCountSlot(); - assertEq( - uint256(vm.load(address(token), adminCountSlot)), 1, "precondition: adminCount is 1" - ); + assertEq(uint256(vm.load(address(token), adminCountSlot)), 1, "precondition: adminCount is 1"); _assertInitialized(address(token), "precondition: initialized marker is set"); vm.prank(admin); token.renounceLastAdmin(); - assertEq( - uint256(vm.load(address(token), adminCountSlot)), 0, "adminCount must be 0 post-renounce" - ); - _assertInitialized( - address(token), - "initialized marker must remain set (renounce only clears adminCount)" - ); + assertEq(uint256(vm.load(address(token), adminCountSlot)), 0, "adminCount must be 0 post-renounce"); + _assertInitialized(address(token), "initialized marker must remain set (renounce only clears adminCount)"); } /// @notice Verifies renounceLastAdmin emits LastAdminRenounced(previousAdmin) diff --git a/test/unit/B20/roles/renounceRole.t.sol b/test/unit/B20/roles/renounceRole.t.sol index 0a8fc1e..8188559 100644 --- a/test/unit/B20/roles/renounceRole.t.sol +++ b/test/unit/B20/roles/renounceRole.t.sol @@ -112,9 +112,7 @@ contract B20RenounceRoleTest is B20Test { uint256(1), "roles[ADMIN][otherAdmin] slot must still be set" ); - assertEq( - uint256(vm.load(address(token), MockB20Storage.adminCountSlot())), 1, "adminCount must drop to 1" - ); + assertEq(uint256(vm.load(address(token), MockB20Storage.adminCountSlot())), 1, "adminCount must drop to 1"); _assertInitialized(address(token), "initialized marker must stay set"); } diff --git a/test/unit/B20Factory/createToken.t.sol b/test/unit/B20Factory/createToken.t.sol index 5489807..30fed40 100644 --- a/test/unit/B20Factory/createToken.t.sol +++ b/test/unit/B20Factory/createToken.t.sol @@ -63,7 +63,9 @@ contract B20FactoryCreateB20Test is B20FactoryTest { IB20Factory.B20CreateParams memory p = _b20Params(); p.version = badVersion; vm.prank(caller); - vm.expectRevert(abi.encodeWithSelector(IB20Factory.UnsupportedVersion.selector, badVersion, IB20Factory.B20Variant.DEFAULT)); + vm.expectRevert( + abi.encodeWithSelector(IB20Factory.UnsupportedVersion.selector, badVersion, IB20Factory.B20Variant.DEFAULT) + ); factory.createB20(IB20Factory.B20Variant.DEFAULT, salt, abi.encode(p), new bytes[](0)); } @@ -78,7 +80,11 @@ contract B20FactoryCreateB20Test is B20FactoryTest { IB20Factory.B20StablecoinCreateParams memory p = _stablecoinParams(); p.version = badVersion; vm.prank(caller); - vm.expectRevert(abi.encodeWithSelector(IB20Factory.UnsupportedVersion.selector, badVersion, IB20Factory.B20Variant.STABLECOIN)); + vm.expectRevert( + abi.encodeWithSelector( + IB20Factory.UnsupportedVersion.selector, badVersion, IB20Factory.B20Variant.STABLECOIN + ) + ); factory.createB20(IB20Factory.B20Variant.STABLECOIN, salt, abi.encode(p), new bytes[](0)); } @@ -108,15 +114,15 @@ contract B20FactoryCreateB20Test is B20FactoryTest { /// @notice Verifies createToken reverts for any unsupported version byte on the SECURITY variant /// @dev Each variant arm has its own version check; this exercises the security arm's check. - function test_createB20_revert_unsupportedVersion_security(address caller, uint8 badVersion, bytes32 salt) - public - { + function test_createB20_revert_unsupportedVersion_security(address caller, uint8 badVersion, bytes32 salt) public { _assumeValidCaller(caller); vm.assume(badVersion != 1); IB20Factory.B20SecurityCreateParams memory p = _securityParams(); p.version = badVersion; vm.prank(caller); - vm.expectRevert(abi.encodeWithSelector(IB20Factory.UnsupportedVersion.selector, badVersion, IB20Factory.B20Variant.SECURITY)); + vm.expectRevert( + abi.encodeWithSelector(IB20Factory.UnsupportedVersion.selector, badVersion, IB20Factory.B20Variant.SECURITY) + ); factory.createB20(IB20Factory.B20Variant.SECURITY, salt, abi.encode(p), new bytes[](0)); } @@ -251,9 +257,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { /// `base.b20.security` namespace and `minimumRedeemable` at the /// `base.b20.redeem` namespace. Paired slot assertions confirm both fields /// land at the expected slots with the correct encodings. - function test_createB20_success_securitySeedsInitialState(address caller, bytes32 salt, uint256 minRedeem) - public - { + function test_createB20_success_securitySeedsInitialState(address caller, bytes32 salt, uint256 minRedeem) public { _assumeValidCaller(caller); IB20Factory.B20SecurityCreateParams memory p = _securityParams("Security Test", "SEC", admin, APPLE_ISIN, minRedeem); @@ -297,11 +301,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { PolicyRegistryConstants.ALWAYS_BLOCK_ID, "redeemPolicyIds slot lane 0 must hold ALWAYS_BLOCK_ID" ); - assertEq( - packed >> 64, - uint256(0), - "redeemPolicyIds slot reserved lanes must be zero on a fresh token" - ); + assertEq(packed >> 64, uint256(0), "redeemPolicyIds slot reserved lanes must be zero on a fresh token"); } /// @notice Verifies the security REDEEM_SENDER_POLICY default does NOT leak into other @@ -310,9 +310,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { /// namespace (`base.b20.redeem`); the base packed policy slots (`transferPolicyIds`, /// `mintPolicyIds` in the base `base.b20` namespace) must remain at their EVM zero /// defaults so the four base scopes still read as ALWAYS_ALLOW_ID. - function test_createB20_success_securityOtherPolicySlotsDefaultToAllow(address caller, bytes32 salt) - public - { + function test_createB20_success_securityOtherPolicySlotsDefaultToAllow(address caller, bytes32 salt) public { _assumeValidCaller(caller); address token = _createSecurity(caller, salt, _securityParams(), new bytes[](0)); @@ -355,9 +353,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { /// in `initCalls`. The privileged-window bypass on the token means the factory-originated /// call succeeds without the role check. Post-creation the slot reflects the overridden /// value, NOT the factory-seeded default. - function test_createB20_success_securityRedeemPolicyOverridableViaInitCall(address caller, bytes32 salt) - public - { + function test_createB20_success_securityRedeemPolicyOverridableViaInitCall(address caller, bytes32 salt) public { _assumeValidCaller(caller); bytes[] memory initCalls = new bytes[](1); initCalls[0] = abi.encodeWithSelector( @@ -404,11 +400,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { assertFalse(MockB20(token).hasRole(B20Constants.DEFAULT_ADMIN_ROLE, caller), "caller must not hold admin"); assertEq(IB20Security(token).securityIdentifier(IDENTIFIER_ISIN), DEFAULT_ISIN, "ISIN must still be set"); - assertEq( - uint256(vm.load(token, MockB20Storage.adminCountSlot())), - 0, - "adminCount must be 0 on zero-admin path" - ); + assertEq(uint256(vm.load(token, MockB20Storage.adminCountSlot())), 0, "adminCount must be 0 on zero-admin path"); _assertInitialized(token, "initialized must still be set on zero-admin path"); } @@ -444,11 +436,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { _stablecoinParams("Test", "TST", admin, xFiat[i]), new bytes[](0) ); - assertEq( - IB20Stablecoin(token).currency(), - xFiat[i], - "multi-country X-prefix fiat code must round-trip" - ); + assertEq(IB20Stablecoin(token).currency(), xFiat[i], "multi-country X-prefix fiat code must round-trip"); } } @@ -471,8 +459,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { /// way the default emitter test pins decimals=18. function test_createB20_success_emitsB20Created_security(address caller, bytes32 salt) public { _assumeValidCaller(caller); - IB20Factory.B20SecurityCreateParams memory p = - _securityParams("Security Test", "SEC", admin, DEFAULT_ISIN, 0); + IB20Factory.B20SecurityCreateParams memory p = _securityParams("Security Test", "SEC", admin, DEFAULT_ISIN, 0); address predicted = factory.getB20Address(IB20Factory.B20Variant.SECURITY, caller, salt); vm.expectEmit(true, true, false, true, address(factory)); @@ -517,9 +504,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { "factory must NOT appear in roles[ADMIN] slot" ); assertEq( - uint256(vm.load(token, MockB20Storage.adminCountSlot())), - 1, - "adminCount must be 1 after bootstrap grant" + uint256(vm.load(token, MockB20Storage.adminCountSlot())), 1, "adminCount must be 1 after bootstrap grant" ); _assertInitialized(token, "initialized marker must be set after bootstrap closes"); } @@ -590,11 +575,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { // Paired slot assertion: packed adminCount lane is 0 (no // bootstrap grant happened) but the initialized bit is still // set (the factory closed the bootstrap window after returning). - assertEq( - uint256(vm.load(token, MockB20Storage.adminCountSlot())), - 0, - "adminCount must be 0 on zero-admin path" - ); + assertEq(uint256(vm.load(token, MockB20Storage.adminCountSlot())), 0, "adminCount must be 0 on zero-admin path"); _assertInitialized(token, "initialized must still be set on zero-admin path"); } @@ -613,11 +594,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { // The stablecoin still got its variant data: currency is set. assertEq(IB20Stablecoin(token).currency(), "USD", "stablecoin currency must still be set"); - assertEq( - uint256(vm.load(token, MockB20Storage.adminCountSlot())), - 0, - "adminCount must be 0 on zero-admin path" - ); + assertEq(uint256(vm.load(token, MockB20Storage.adminCountSlot())), 0, "adminCount must be 0 on zero-admin path"); _assertInitialized(token, "initialized must still be set on zero-admin path"); assertEq( vm.load(token, MockB20StablecoinStorage.currencySlot()), diff --git a/test/unit/B20Factory/getTokenAddress.t.sol b/test/unit/B20Factory/getTokenAddress.t.sol index 37adc07..8a69761 100644 --- a/test/unit/B20Factory/getTokenAddress.t.sol +++ b/test/unit/B20Factory/getTokenAddress.t.sol @@ -11,10 +11,7 @@ contract B20FactoryGetTokenAddressTest is B20FactoryTest { /// is happy with the raw byte but Solidity reverts at function entry on an /// out-of-range enum input from a fuzzer. function _boundVariant(uint8 variantInt) internal pure returns (IB20Factory.B20Variant) { - return - IB20Factory.B20Variant( - uint8(bound(uint256(variantInt), 0, uint256(type(IB20Factory.B20Variant).max))) - ); + return IB20Factory.B20Variant(uint8(bound(uint256(variantInt), 0, uint256(type(IB20Factory.B20Variant).max)))); } /// @notice Verifies getTokenAddress is deterministic for the same inputs diff --git a/test/unit/B20FactoryLib/buildRoleGrants.t.sol b/test/unit/B20FactoryLib/buildRoleGrants.t.sol index 9e7974c..823a532 100644 --- a/test/unit/B20FactoryLib/buildRoleGrants.t.sol +++ b/test/unit/B20FactoryLib/buildRoleGrants.t.sol @@ -174,9 +174,7 @@ contract B20FactoryLibBuildRoleGrantsTest is B20FactoryLibTest { assertEq(result[3], abi.encodeCall(IB20.grantRole, (B20Constants.PAUSE_ROLE, pauser_)), "3: PAUSE_ROLE"); assertEq(result[4], abi.encodeCall(IB20.grantRole, (B20Constants.UNPAUSE_ROLE, unpauser_)), "4: UNPAUSE_ROLE"); assertEq( - result[5], - abi.encodeCall(IB20.grantRole, (B20Constants.METADATA_ROLE, metadataAdmin_)), - "5: METADATA_ROLE" + result[5], abi.encodeCall(IB20.grantRole, (B20Constants.METADATA_ROLE, metadataAdmin_)), "5: METADATA_ROLE" ); } @@ -279,9 +277,7 @@ contract B20FactoryLibBuildRoleGrantsTest is B20FactoryLibTest { assertEq(result[4], abi.encodeCall(IB20.grantRole, (B20Constants.PAUSE_ROLE, pauser_)), "4: PAUSE_ROLE"); assertEq(result[5], abi.encodeCall(IB20.grantRole, (B20Constants.UNPAUSE_ROLE, unpauser_)), "5: UNPAUSE_ROLE"); assertEq( - result[6], - abi.encodeCall(IB20.grantRole, (B20Constants.METADATA_ROLE, metadataAdmin_)), - "6: METADATA_ROLE" + result[6], abi.encodeCall(IB20.grantRole, (B20Constants.METADATA_ROLE, metadataAdmin_)), "6: METADATA_ROLE" ); assertEq( result[7], diff --git a/test/unit/B20FactoryLib/buildSecurityIdentifierUpdates.t.sol b/test/unit/B20FactoryLib/buildSecurityIdentifierUpdates.t.sol index 2c78365..a989d62 100644 --- a/test/unit/B20FactoryLib/buildSecurityIdentifierUpdates.t.sol +++ b/test/unit/B20FactoryLib/buildSecurityIdentifierUpdates.t.sol @@ -29,9 +29,7 @@ contract B20FactoryLibBuildSecurityIdentifierUpdatesTest is B20FactoryLibTest { /// `identifierValues` differ in length. /// @dev Mirrors the length-check semantics of /// `buildRoleGrants(bytes32[], address[])`. - function test_buildSecurityIdentifierUpdates_revert_lengthMismatch(uint8 typesLenSeed, uint8 valuesLenSeed) - public - { + function test_buildSecurityIdentifierUpdates_revert_lengthMismatch(uint8 typesLenSeed, uint8 valuesLenSeed) public { uint256 typesLen = bound(uint256(typesLenSeed), 0, 16); uint256 valuesLen = bound(uint256(valuesLenSeed), 0, 16); vm.assume(typesLen != valuesLen); diff --git a/test/unit/B20FactoryLib/encodeDefaultCreateParams.t.sol b/test/unit/B20FactoryLib/encodeDefaultCreateParams.t.sol index d2e20b6..21d15bb 100644 --- a/test/unit/B20FactoryLib/encodeDefaultCreateParams.t.sol +++ b/test/unit/B20FactoryLib/encodeDefaultCreateParams.t.sol @@ -37,10 +37,7 @@ contract B20FactoryLibEncodeDefaultCreateParamsTest is B20FactoryLibTest { ) public pure { bytes memory expected = abi.encode( IB20Factory.B20CreateParams({ - version: B20FactoryLib.B20_CREATE_PARAMS_VERSION, - name: name, - symbol: symbol, - initialAdmin: initialAdmin + version: B20FactoryLib.B20_CREATE_PARAMS_VERSION, name: name, symbol: symbol, initialAdmin: initialAdmin }) ); bytes memory actual = B20FactoryLib.encodeDefaultCreateParams(name, symbol, initialAdmin); diff --git a/test/unit/B20FactoryLib/encodeSecurityCreateParams.t.sol b/test/unit/B20FactoryLib/encodeSecurityCreateParams.t.sol index ac679ab..eabf720 100644 --- a/test/unit/B20FactoryLib/encodeSecurityCreateParams.t.sol +++ b/test/unit/B20FactoryLib/encodeSecurityCreateParams.t.sol @@ -19,10 +19,10 @@ contract B20FactoryLibEncodeSecurityCreateParamsTest is B20FactoryLibTest { string memory isin, uint256 minimumRedeemable ) public pure { - bytes memory blob = - B20FactoryLib.encodeSecurityCreateParams(name, symbol, initialAdmin, isin, minimumRedeemable); - IB20Factory.B20SecurityCreateParams memory decoded = - abi.decode(blob, (IB20Factory.B20SecurityCreateParams)); + bytes memory blob = B20FactoryLib.encodeSecurityCreateParams( + name, symbol, initialAdmin, isin, minimumRedeemable + ); + IB20Factory.B20SecurityCreateParams memory decoded = abi.decode(blob, (IB20Factory.B20SecurityCreateParams)); assertEq( decoded.version, diff --git a/test/unit/B20FactoryLib/encodeStablecoinCreateParams.t.sol b/test/unit/B20FactoryLib/encodeStablecoinCreateParams.t.sol index 375eb93..a0d1488 100644 --- a/test/unit/B20FactoryLib/encodeStablecoinCreateParams.t.sol +++ b/test/unit/B20FactoryLib/encodeStablecoinCreateParams.t.sol @@ -19,8 +19,7 @@ contract B20FactoryLibEncodeStablecoinCreateParamsTest is B20FactoryLibTest { string memory currency ) public pure { bytes memory blob = B20FactoryLib.encodeStablecoinCreateParams(name, symbol, initialAdmin, currency); - IB20Factory.B20StablecoinCreateParams memory decoded = - abi.decode(blob, (IB20Factory.B20StablecoinCreateParams)); + IB20Factory.B20StablecoinCreateParams memory decoded = abi.decode(blob, (IB20Factory.B20StablecoinCreateParams)); assertEq( decoded.version, diff --git a/test/unit/B20FactoryLib/encodeUpdatePolicy.t.sol b/test/unit/B20FactoryLib/encodeUpdatePolicy.t.sol index 5f81f78..f058740 100644 --- a/test/unit/B20FactoryLib/encodeUpdatePolicy.t.sol +++ b/test/unit/B20FactoryLib/encodeUpdatePolicy.t.sol @@ -11,10 +11,7 @@ contract B20FactoryLibEncodeUpdatePolicyTest is B20FactoryLibTest { /// @dev Pins both the selector and the (bytes32, uint64) argument /// order. A swapped-arg regression would land scope bytes in /// the policy-id slot and vice versa. - function test_encodeUpdatePolicy_success_matchesAbiEncodeCall(bytes32 policyScope, uint64 newPolicyId) - public - pure - { + function test_encodeUpdatePolicy_success_matchesAbiEncodeCall(bytes32 policyScope, uint64 newPolicyId) public pure { bytes memory expected = abi.encodeCall(IB20.updatePolicy, (policyScope, newPolicyId)); bytes memory actual = B20FactoryLib.encodeUpdatePolicy(policyScope, newPolicyId); assertEq(actual, expected, "init-call must match abi.encodeCall(IB20.updatePolicy, ...)"); diff --git a/test/unit/PolicyRegistry/writeBuiltins.t.sol b/test/unit/PolicyRegistry/writeBuiltins.t.sol index 54dec68..4ee97bd 100644 --- a/test/unit/PolicyRegistry/writeBuiltins.t.sol +++ b/test/unit/PolicyRegistry/writeBuiltins.t.sol @@ -30,16 +30,14 @@ contract PolicyRegistryWriteBuiltinsTest is PolicyRegistryTest { // Sanity: sentinel slots start empty before any create. assertEq( vm.load( - address(policyRegistry), - MockPolicyRegistryStorage.policySlot(PolicyRegistryConstants.ALWAYS_ALLOW_ID) + address(policyRegistry), MockPolicyRegistryStorage.policySlot(PolicyRegistryConstants.ALWAYS_ALLOW_ID) ), bytes32(0), "ALWAYS_ALLOW_ID slot must be empty before init" ); assertEq( vm.load( - address(policyRegistry), - MockPolicyRegistryStorage.policySlot(PolicyRegistryConstants.ALWAYS_BLOCK_ID) + address(policyRegistry), MockPolicyRegistryStorage.policySlot(PolicyRegistryConstants.ALWAYS_BLOCK_ID) ), bytes32(0), "ALWAYS_BLOCK_ID slot must be empty before init" diff --git a/test/unit/storage/B20FullLayout.t.sol b/test/unit/storage/B20FullLayout.t.sol index a379911..e16dbe9 100644 --- a/test/unit/storage/B20FullLayout.t.sol +++ b/test/unit/storage/B20FullLayout.t.sol @@ -57,6 +57,7 @@ contract B20FullLayoutTest is B20Test { uint64 internal transferReceiverMarker; uint64 internal transferExecutorMarker; uint64 internal mintReceiverMarker; + /// @notice Cross-cuts every field of MockB20Storage.Layout in a single /// populated snapshot. /// @dev Setup writes non-default values to every reachable storage @@ -179,11 +180,7 @@ contract B20FullLayoutTest is B20Test { assertEq(packedTransfer >> 192, 0, "slot 9 bits 192..255: reserved lane must be zero"); uint256 packedMint = uint256(vm.load(tokenAddr, MockB20Storage.mintPolicyIdsSlot())); - assertEq( - packedMint & 0xFFFFFFFFFFFFFFFF, - uint256(mintReceiverMarker), - "slot 10 bits 0..63: mint RECEIVER lane" - ); + assertEq(packedMint & 0xFFFFFFFFFFFFFFFF, uint256(mintReceiverMarker), "slot 10 bits 0..63: mint RECEIVER lane"); assertEq(packedMint >> 64, 0, "slot 10 bits 64..255: three reserved lanes must be zero"); // ---------- pausedVectors (slot 11) ---------- @@ -198,7 +195,9 @@ contract B20FullLayoutTest is B20Test { assertEq(pausedRaw, expectedPaused, "slot 11: pausedVectors must hold exactly the four defined bits"); // No bits set outside the defined PausableFeature range. Computed // as the complement of the union of all defined bits. - assertEq(pausedRaw & ~expectedPaused, 0, "slot 11: no bits may be set outside the defined PausableFeature range"); + assertEq( + pausedRaw & ~expectedPaused, 0, "slot 11: no bits may be set outside the defined PausableFeature range" + ); // ---------- supplyCap (slot 12) ---------- assertEq(uint256(vm.load(tokenAddr, MockB20Storage.supplyCapSlot())), token.supplyCap(), "slot 12: supplyCap"); @@ -265,14 +264,12 @@ contract B20FullLayoutTest is B20Test { // precondition rejects arbitrary uint64s, so we can't use synthetic // hex markers like `0x1111...`. Mixing ALLOWLIST + BLOCKLIST types // makes the top byte vary between lanes too, not just the counter. - transferSenderMarker = - StdPrecompiles.POLICY_REGISTRY.createPolicy(admin, IPolicyRegistry.PolicyType.ALLOWLIST); + transferSenderMarker = StdPrecompiles.POLICY_REGISTRY.createPolicy(admin, IPolicyRegistry.PolicyType.ALLOWLIST); transferReceiverMarker = StdPrecompiles.POLICY_REGISTRY.createPolicy(admin, IPolicyRegistry.PolicyType.BLOCKLIST); transferExecutorMarker = StdPrecompiles.POLICY_REGISTRY.createPolicy(admin, IPolicyRegistry.PolicyType.ALLOWLIST); - mintReceiverMarker = - StdPrecompiles.POLICY_REGISTRY.createPolicy(admin, IPolicyRegistry.PolicyType.BLOCKLIST); + mintReceiverMarker = StdPrecompiles.POLICY_REGISTRY.createPolicy(admin, IPolicyRegistry.PolicyType.BLOCKLIST); _setPolicy(B20Constants.TRANSFER_SENDER_POLICY, transferSenderMarker); _setPolicy(B20Constants.TRANSFER_RECEIVER_POLICY, transferReceiverMarker); _setPolicy(B20Constants.TRANSFER_EXECUTOR_POLICY, transferExecutorMarker); diff --git a/test/unit/storage/B20SecurityFullLayout.t.sol b/test/unit/storage/B20SecurityFullLayout.t.sol index 7cbc1f0..0055137 100644 --- a/test/unit/storage/B20SecurityFullLayout.t.sol +++ b/test/unit/storage/B20SecurityFullLayout.t.sol @@ -139,11 +139,7 @@ contract B20SecurityFullLayoutTest is B20SecurityTest { uint256(redeemSenderMarker), "redeem slot 1 bits 0..63: REDEEM_SENDER_POLICY lane must hold the marker" ); - assertEq( - packedRedeem >> 64, - uint256(0), - "redeem slot 1 bits 64..255: three reserved lanes must be zero" - ); + assertEq(packedRedeem >> 64, uint256(0), "redeem slot 1 bits 64..255: three reserved lanes must be zero"); } /// @notice Verifies the factory-seeded default in `redeemPolicyIds` @@ -154,18 +150,13 @@ contract B20SecurityFullLayoutTest is B20SecurityTest { /// (the populate's `_setRedeemPolicy` would overwrite whatever /// the seed left behind). function test_b20SecurityLayout_success_freshTokenSeedsRedeemPolicyToBlock() public view { - uint256 packedRedeem = - uint256(vm.load(address(token), MockB20RedeemStorage.redeemPolicyIdsSlot())); + uint256 packedRedeem = uint256(vm.load(address(token), MockB20RedeemStorage.redeemPolicyIdsSlot())); assertEq( packedRedeem & 0xFFFFFFFFFFFFFFFF, uint256(PolicyRegistryConstants.ALWAYS_BLOCK_ID), "fresh token: redeem slot 1 bits 0..63 must be ALWAYS_BLOCK_ID from factory seed" ); - assertEq( - packedRedeem >> 64, - uint256(0), - "fresh token: redeem slot 1 bits 64..255 reserved lanes must be zero" - ); + assertEq(packedRedeem >> 64, uint256(0), "fresh token: redeem slot 1 bits 64..255 reserved lanes must be zero"); } /// @notice Verifies the `base.b20.security` and `base.b20.redeem` @@ -208,8 +199,7 @@ contract B20SecurityFullLayoutTest is B20SecurityTest { // uint64 marker. Using a fresh real policy (distinct from the // ALWAYS_BLOCK_ID default) gives us a recognizable post-write // observable. - redeemSenderMarker = - StdPrecompiles.POLICY_REGISTRY.createPolicy(admin, IPolicyRegistry.PolicyType.ALLOWLIST); + redeemSenderMarker = StdPrecompiles.POLICY_REGISTRY.createPolicy(admin, IPolicyRegistry.PolicyType.ALLOWLIST); _setRedeemPolicy(redeemSenderMarker); } } diff --git a/test/unit/storage/PolicyRegistryFullLayout.t.sol b/test/unit/storage/PolicyRegistryFullLayout.t.sol index 04c5ca8..77b6c47 100644 --- a/test/unit/storage/PolicyRegistryFullLayout.t.sol +++ b/test/unit/storage/PolicyRegistryFullLayout.t.sol @@ -93,9 +93,7 @@ contract PolicyRegistryFullLayoutTest is PolicyRegistryTest { uint256(0), "policies[allowlistId] bits 160..254: reserved must be zero" ); - assertEq( - packedA >> 255, uint256(1), "policies[allowlistId] bit 255: exists flag must be set" - ); + assertEq(packedA >> 255, uint256(1), "policies[allowlistId] bit 255: exists flag must be set"); assertEq( MockPolicyRegistryStorage.policyTypeFromId(allowlistId), uint8(IPolicyRegistry.PolicyType.ALLOWLIST), @@ -115,9 +113,7 @@ contract PolicyRegistryFullLayoutTest is PolicyRegistryTest { uint256(0), "policies[blocklistId] bits 160..254: reserved must be zero" ); - assertEq( - packedB >> 255, uint256(1), "policies[blocklistId] bit 255: exists flag must be set" - ); + assertEq(packedB >> 255, uint256(1), "policies[blocklistId] bit 255: exists flag must be set"); assertEq( MockPolicyRegistryStorage.policyTypeFromId(blocklistId), uint8(IPolicyRegistry.PolicyType.BLOCKLIST), From 4198e024b226faa378e5fc8fd589e909603c0f5b Mon Sep 17 00:00:00 2001 From: Eric Shenghsiung Liu Date: Wed, 27 May 2026 09:46:37 -0400 Subject: [PATCH 4/8] Pin foundry-toolchain to stable; fix B20FactoryLibTest formatting Nightly foundry has different fmt rules than stable. Pinning to stable aligns CI format checks with local development. Also fixes a 403 rate-limit issue where foundryup would fail fetching release tags from the GitHub API. --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43a61e0..730e1b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,8 @@ jobs: submodules: recursive - uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable - name: Build run: forge build @@ -31,6 +33,8 @@ jobs: submodules: recursive - uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable - name: Run tests run: forge test -v @@ -44,6 +48,8 @@ jobs: submodules: recursive - uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable - name: Check formatting run: forge fmt --check @@ -57,6 +63,8 @@ jobs: submodules: recursive - uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable - name: Generate coverage report run: forge coverage --report lcov From 63d10934a8132fc4479982a212c7df795ee04261 Mon Sep 17 00:00:00 2001 From: Eric Shenghsiung Liu Date: Wed, 27 May 2026 09:52:54 -0400 Subject: [PATCH 5/8] Fix formatting for forge v1.7.1 stable The CI stable toolchain is v1.7.1 which removes trailing blank lines before closing braces in empty contract bodies. Update local format to match. --- test/lib/B20FactoryLibTest.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/lib/B20FactoryLibTest.sol b/test/lib/B20FactoryLibTest.sol index b74d5c7..7eefc7e 100644 --- a/test/lib/B20FactoryLibTest.sol +++ b/test/lib/B20FactoryLibTest.sol @@ -18,5 +18,4 @@ import {BaseTest} from "test/lib/BaseTest.sol"; /// the rest of the suite. No `setUp` extension is needed. contract B20FactoryLibTest is BaseTest { // No additional state; `BaseTest`'s actor labels and helpers are sufficient. - - } +} From 44f90a478937dd3aadc6ce4449f34ee3d67ecd43 Mon Sep 17 00:00:00 2001 From: Eric Shenghsiung Liu Date: Wed, 27 May 2026 10:59:08 -0400 Subject: [PATCH 6/8] Remove coverage job Not actively monitored and Codecov token is not set up. --- .github/workflows/ci.yml | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 730e1b9..ff8f3f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,26 +53,3 @@ jobs: - name: Check formatting run: forge fmt --check - - coverage: - name: Coverage - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - - uses: foundry-rs/foundry-toolchain@v1 - with: - version: stable - - - name: Generate coverage report - run: forge coverage --report lcov - - - name: Upload to Codecov - uses: codecov/codecov-action@v4 - with: - files: lcov.info - fail_ci_if_error: false - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From 902edc2b7f923cf8d59c151cdd2afa3610134253 Mon Sep 17 00:00:00 2001 From: Eric Shenghsiung Liu Date: Wed, 27 May 2026 12:31:38 -0400 Subject: [PATCH 7/8] Preserve single-line slot helpers in MockB20SecurityStorage Use forgefmt: disable-next-item on each function to prevent forge fmt from expanding the compact lookup-table form. --- test/lib/mocks/MockB20Storage.sol | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/test/lib/mocks/MockB20Storage.sol b/test/lib/mocks/MockB20Storage.sol index ef27492..5c4fcf6 100644 --- a/test/lib/mocks/MockB20Storage.sol +++ b/test/lib/mocks/MockB20Storage.sol @@ -407,17 +407,12 @@ library MockB20SecurityStorage { // TOP-LEVEL FIELD SLOTS // ============================================================ - function sharesToTokensRatioSlot() internal pure returns (bytes32) { - return slotOf(SHARES_TO_TOKENS_RATIO_OFFSET); - } - - function usedAnnouncementIdsBaseSlot() internal pure returns (bytes32) { - return slotOf(USED_ANNOUNCEMENT_IDS_OFFSET); - } - - function identifiersBaseSlot() internal pure returns (bytes32) { - return slotOf(IDENTIFIERS_OFFSET); - } + // forgefmt: disable-next-item + function sharesToTokensRatioSlot() internal pure returns (bytes32) { return slotOf(SHARES_TO_TOKENS_RATIO_OFFSET); } + // forgefmt: disable-next-item + function usedAnnouncementIdsBaseSlot() internal pure returns (bytes32) { return slotOf(USED_ANNOUNCEMENT_IDS_OFFSET); } + // forgefmt: disable-next-item + function identifiersBaseSlot() internal pure returns (bytes32) { return slotOf(IDENTIFIERS_OFFSET); } // ============================================================ // MAPPING MEMBER SLOTS From 1c8e28b637e57ab7bf334dd40404092a3ce5d0c1 Mon Sep 17 00:00:00 2001 From: Eric Shenghsiung Liu Date: Wed, 27 May 2026 12:51:40 -0400 Subject: [PATCH 8/8] Use forgefmt disable-start/end for MockB20SecurityStorage slot helpers Switch from per-function disable-next-item to a single block, and add the same to minimumRedeemableSlot / redeemPolicyIdsSlot. --- test/lib/mocks/MockB20Storage.sol | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/test/lib/mocks/MockB20Storage.sol b/test/lib/mocks/MockB20Storage.sol index 5c4fcf6..cd8e161 100644 --- a/test/lib/mocks/MockB20Storage.sol +++ b/test/lib/mocks/MockB20Storage.sol @@ -407,13 +407,13 @@ library MockB20SecurityStorage { // TOP-LEVEL FIELD SLOTS // ============================================================ - // forgefmt: disable-next-item + // forgefmt: disable-start function sharesToTokensRatioSlot() internal pure returns (bytes32) { return slotOf(SHARES_TO_TOKENS_RATIO_OFFSET); } - // forgefmt: disable-next-item function usedAnnouncementIdsBaseSlot() internal pure returns (bytes32) { return slotOf(USED_ANNOUNCEMENT_IDS_OFFSET); } - // forgefmt: disable-next-item function identifiersBaseSlot() internal pure returns (bytes32) { return slotOf(IDENTIFIERS_OFFSET); } + // forgefmt: disable-end + // ============================================================ // MAPPING MEMBER SLOTS // ============================================================ @@ -496,13 +496,10 @@ library MockB20RedeemStorage { // TOP-LEVEL FIELD SLOTS // ============================================================ - function minimumRedeemableSlot() internal pure returns (bytes32) { - return slotOf(MINIMUM_REDEEMABLE_OFFSET); - } - - function redeemPolicyIdsSlot() internal pure returns (bytes32) { - return slotOf(REDEEM_POLICY_IDS_OFFSET); - } + // forgefmt: disable-start + function minimumRedeemableSlot() internal pure returns (bytes32) { return slotOf(MINIMUM_REDEEMABLE_OFFSET); } + function redeemPolicyIdsSlot() internal pure returns (bytes32) { return slotOf(REDEEM_POLICY_IDS_OFFSET); } + // forgefmt: disable-end } /// @title MockB20StablecoinStorage