Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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
with:
version: stable

- name: Build
run: forge build

- name: Contract sizes (informational)
run: forge build --sizes || true

test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- uses: foundry-rs/foundry-toolchain@v1
with:
version: stable

- name: Run tests
run: forge test -v

fmt:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if CI runs in order of definition or parallelizes, but fmt should be before test if sequential given it will fail faster

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parallelizes

name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- uses: foundry-rs/foundry-toolchain@v1
with:
version: stable

- name: Check formatting
run: forge fmt --check
2 changes: 1 addition & 1 deletion src/interfaces/IB20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 2 additions & 9 deletions src/lib/B20FactoryLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
);
}
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion test/lib/B20FactoryLibTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ 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.
}
3 changes: 1 addition & 2 deletions test/lib/PolicyRegistryTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
6 changes: 1 addition & 5 deletions test/lib/mocks/MockB20Factory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

// ============================================================
Expand Down
12 changes: 10 additions & 2 deletions test/lib/mocks/MockB20Storage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -407,8 +407,11 @@ library MockB20SecurityStorage {
// TOP-LEVEL FIELD SLOTS
// ============================================================

// 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); }

// ============================================================
Expand Down Expand Up @@ -493,8 +496,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
Expand Down
13 changes: 3 additions & 10 deletions test/unit/B20/roles/renounceLastAdmin.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 1 addition & 3 deletions test/unit/B20/roles/renounceRole.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down
67 changes: 22 additions & 45 deletions test/unit/B20Factory/createToken.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand All @@ -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));
}

Expand Down Expand Up @@ -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));
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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));

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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");
}

Expand Down Expand Up @@ -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");
}
}

Expand All @@ -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));
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -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");
}

Expand All @@ -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()),
Expand Down
5 changes: 1 addition & 4 deletions test/unit/B20Factory/getTokenAddress.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 2 additions & 6 deletions test/unit/B20FactoryLib/buildRoleGrants.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
}

Expand Down Expand Up @@ -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],
Expand Down
4 changes: 1 addition & 3 deletions test/unit/B20FactoryLib/buildSecurityIdentifierUpdates.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading