From f00e46ffd6cd0e0f68ae54aa5e08fa56998a8967 Mon Sep 17 00:00:00 2001 From: Conner Swenberg Date: Mon, 1 Jun 2026 21:14:41 -0400 Subject: [PATCH] fix(factory): use MissingRequiredField for empty stablecoin currency (BOP-261) Matches the Rust precompile, which surfaces an empty-string currency as a missing required field rather than an invalid format. Non-empty-but- malformed currencies still revert with InvalidCurrency. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/interfaces/IB20Factory.sol | 8 ++++---- test/lib/mocks/MockB20Factory.sol | 4 ++-- test/unit/B20Factory/createToken.t.sol | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/interfaces/IB20Factory.sol b/src/interfaces/IB20Factory.sol index 10c1bef..59bfe48 100644 --- a/src/interfaces/IB20Factory.sol +++ b/src/interfaces/IB20Factory.sol @@ -76,10 +76,10 @@ interface IB20Factory { /// @notice A required string argument was the empty string. /// - /// @param field Name of the missing field (e.g. `"isin"`). + /// @param field Name of the missing field (e.g. `"currency"`). error MissingRequiredField(string field); - /// @notice The stablecoin `currency` contained a non-`A`-`Z` byte. + /// @notice The stablecoin `currency` was non-empty but contained a non-`A`-`Z` byte. error InvalidCurrency(string code); /// @notice One of the `initCalls` reverted. The factory bubbles the underlying revert reason @@ -116,8 +116,8 @@ interface IB20Factory { /// @dev Reverts with IActivationRegistry.FeatureNotActivated when the variant feature is not activated. /// @dev Reverts with `InvalidVariant` when `variant` is outside the `B20Variant` range. /// @dev Reverts with `UnsupportedVersion` when the leading `version` byte in `params` is unrecognized for `variant`. - /// @dev Reverts with `MissingRequiredField` when a required string field is empty. - /// @dev Reverts with `InvalidCurrency` when a stablecoin `currency` contains a non-`A`-`Z` byte. + /// @dev Reverts with `MissingRequiredField` when a required string field is empty (e.g. stablecoin `currency` or security `isin`). + /// @dev Reverts with `InvalidCurrency` when a stablecoin `currency` is non-empty but contains a non-`A`-`Z` byte. /// @dev Reverts with `TokenAlreadyExists` when a token already exists at the derived address. /// @dev Reverts with `InitCallFailed` (or the bubbled inner reason) when any entry in `initCalls` reverts. /// diff --git a/test/lib/mocks/MockB20Factory.sol b/test/lib/mocks/MockB20Factory.sol index 408c815..ccdb6f7 100644 --- a/test/lib/mocks/MockB20Factory.sol +++ b/test/lib/mocks/MockB20Factory.sol @@ -138,9 +138,9 @@ contract MockB20Factory is IB20Factory { } // Empty currency must be rejected explicitly: the format-check loop below has // no bytes to inspect on empty input and would vacuously succeed otherwise. - // Reverts InvalidCurrency("") to match the Rust precompile's selector. + // Reverts MissingRequiredField("currency") to match the Rust precompile's selector. bytes memory cb = bytes(p.currency); - if (cb.length == 0) revert InvalidCurrency(p.currency); + if (cb.length == 0) revert MissingRequiredField("currency"); // Format check: every byte must be an uppercase ASCII letter (A-Z). for (uint256 i = 0; i < cb.length; ++i) { if (cb[i] < 0x41 || cb[i] > 0x5A) revert InvalidCurrency(p.currency); diff --git a/test/unit/B20Factory/createToken.t.sol b/test/unit/B20Factory/createToken.t.sol index 63eb306..8f2890b 100644 --- a/test/unit/B20Factory/createToken.t.sol +++ b/test/unit/B20Factory/createToken.t.sol @@ -79,7 +79,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { /// @notice Any non-empty string containing a non-`A`–`Z` byte reverts with `InvalidCurrency(code)`. /// @dev Subsumes every point case (lowercase, digits, symbols, multi-byte UTF-8) via /// `vm.assume(!_isValidFiatCode)`. Empty input is covered by - /// `test_createB20_revert_emptyCurrency` (rejected with `InvalidCurrency("")`). + /// `test_createB20_revert_emptyCurrency` (rejected with `MissingRequiredField("currency")`). function test_createB20_revert_currency_rejectsInvalidFormat(string memory code, address caller, bytes32 salt) public { @@ -133,12 +133,12 @@ contract B20FactoryCreateB20Test is B20FactoryTest { /// @notice Verifies stablecoin createToken reverts when currency is the empty string /// @dev The format-check loop on `currency` is vacuously safe on empty input (no bytes /// to inspect), so an explicit length check is required to reject empty up front. - /// Checks InvalidCurrency("") error, matching the Rust precompile's selector. + /// Checks MissingRequiredField("currency") error, matching the Rust precompile's selector. function test_createB20_revert_emptyCurrency(address caller, bytes32 salt) public { _assumeValidCaller(caller); IB20Factory.B20StablecoinCreateParams memory p = _stablecoinParams("Stablecoin Test", "USD", admin, ""); vm.prank(caller); - vm.expectRevert(abi.encodeWithSelector(IB20Factory.InvalidCurrency.selector, "")); + vm.expectRevert(abi.encodeWithSelector(IB20Factory.MissingRequiredField.selector, "currency")); factory.createB20(IB20Factory.B20Variant.STABLECOIN, salt, abi.encode(p), new bytes[](0)); }