diff --git a/src/interfaces/IB20Factory.sol b/src/interfaces/IB20Factory.sol index 269aad21..6b00af62 100644 --- a/src/interfaces/IB20Factory.sol +++ b/src/interfaces/IB20Factory.sol @@ -171,10 +171,14 @@ interface IB20Factory { /// Caller must use a different salt. error TokenAlreadyExists(address token); - /// @notice `variant` is not a recognized `B20Variant`. Reached - /// only when the factory is invoked with a raw variant byte - /// outside the enum range (typed `createToken` callers are - /// rejected by ABI decoding before this check fires). + /// @notice `variant` is not a recognized `B20Variant`. Raised when + /// the factory is invoked with a `variant` byte outside the + /// enum range (`> uint8(type(B20Variant).max)`). The variant + /// parameter is typed as `uint8` (not the enum) on both + /// `createB20` and `getB20Address` so the factory body runs + /// this check before any decode-time panic, matching the + /// Rust precompile's `IB20Factory::InvalidVariant {}` revert + /// selector for every caller path (typed or raw-calldata). error InvalidVariant(); /// @notice The leading `version` byte in `params` does not match @@ -249,6 +253,12 @@ interface IB20Factory { /// effects appear strictly after the creation event in the /// log order. /// @param variant Which variant struct `params` decodes as. + /// Typed as `uint8` (not the `B20Variant` enum) + /// so the factory body — not the ABI decoder — + /// rejects out-of-range bytes with the typed + /// `InvalidVariant()` revert, matching the Rust + /// precompile's selector. Solidity callers MUST + /// pass `uint8(B20Variant.X)`. /// @param salt Caller-chosen salt for deterministic address /// derivation. /// @param params ABI-encoded variant-specific creation struct, @@ -259,7 +269,7 @@ interface IB20Factory { /// token-side authorization checks are bypassed /// for this window only. /// @return token The address of the newly created token. - function createB20(B20Variant variant, bytes32 salt, bytes calldata params, bytes[] calldata initCalls) + function createB20(uint8 variant, bytes32 salt, bytes calldata params, bytes[] calldata initCalls) external returns (address token); @@ -272,7 +282,12 @@ interface IB20Factory { /// depends only on these inputs; the remaining `params` /// fields (including decimals, which are fixed by variant) /// do not affect the address. - function getB20Address(B20Variant variant, address sender, bytes32 salt) external view returns (address); + /// @dev `variant` is typed as `uint8` (not the `B20Variant` enum) + /// so out-of-range bytes revert with `InvalidVariant()` + /// (matching the Rust precompile) rather than a decode-time + /// `Panic(0x21)`. Solidity callers MUST pass + /// `uint8(B20Variant.X)`. + function getB20Address(uint8 variant, address sender, bytes32 salt) external view returns (address); /// @notice Whether `token` was created by this factory. Recovered /// from the address prefix (bytes `[0:10]`); no storage read. diff --git a/test/lib/B20FactoryTest.sol b/test/lib/B20FactoryTest.sol index 3a19dd00..115fba83 100644 --- a/test/lib/B20FactoryTest.sol +++ b/test/lib/B20FactoryTest.sol @@ -111,7 +111,7 @@ contract B20FactoryTest is BaseTest { bytes[] memory initCalls ) internal returns (address token) { vm.prank(caller); - return factory.createB20(IB20Factory.B20Variant.DEFAULT, salt, abi.encode(params), initCalls); + return factory.createB20(uint8(IB20Factory.B20Variant.DEFAULT), salt, abi.encode(params), initCalls); } /// @notice Create a default-variant token with defaults (alice creator, fresh salt, empty init calls). @@ -127,7 +127,7 @@ contract B20FactoryTest is BaseTest { bytes[] memory initCalls ) internal returns (address token) { vm.prank(caller); - return factory.createB20(IB20Factory.B20Variant.STABLECOIN, salt, abi.encode(params), initCalls); + return factory.createB20(uint8(IB20Factory.B20Variant.STABLECOIN), salt, abi.encode(params), initCalls); } /// @notice Create a stablecoin-variant token with defaults. @@ -143,7 +143,7 @@ contract B20FactoryTest is BaseTest { bytes[] memory initCalls ) internal returns (address token) { vm.prank(caller); - return factory.createB20(IB20Factory.B20Variant.SECURITY, salt, abi.encode(params), initCalls); + return factory.createB20(uint8(IB20Factory.B20Variant.SECURITY), salt, abi.encode(params), initCalls); } /// @notice Create a security-variant token with defaults. diff --git a/test/lib/mocks/MockB20Factory.sol b/test/lib/mocks/MockB20Factory.sol index ddf2ec44..c78f953c 100644 --- a/test/lib/mocks/MockB20Factory.sol +++ b/test/lib/mocks/MockB20Factory.sol @@ -94,10 +94,19 @@ contract MockB20Factory is IB20Factory { Vm internal constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); /// @inheritdoc IB20Factory - function createB20(B20Variant variant, bytes32 salt, bytes calldata params, bytes[] calldata initCalls) + /// @dev `variant` is taken as `uint8` (not the `B20Variant` enum) so the entry-point + /// guard below — not the ABI decoder — rejects out-of-range bytes with the typed + /// `InvalidVariant()` selector, matching the Rust precompile's + /// `B20Variant::from_abi(call.variant).ok_or_else(... InvalidVariant {})` guard. + /// The cast `B20Variant(variant)` is inlined at each usage site rather than bound + /// to a local so this function stays under Solidity's stack-too-deep limit. + function createB20(uint8 variant, bytes32 salt, bytes calldata params, bytes[] calldata initCalls) external returns (address token) { + // -- 0. Reject out-of-range variant bytes with the typed selector. + if (variant > uint8(type(B20Variant).max)) revert InvalidVariant(); + // -- 1. Decode + validate, get the common params -- string memory name_; string memory symbol_; @@ -107,17 +116,19 @@ contract MockB20Factory is IB20Factory { string memory isin_; uint256 minimumRedeemable_; - if (variant == B20Variant.DEFAULT) { + if (B20Variant(variant) == B20Variant.DEFAULT) { B20CreateParams memory p = abi.decode(params, (B20CreateParams)); - if (p.version != B20FactoryLib.B20_CREATE_PARAMS_VERSION) revert UnsupportedVersion(p.version, variant); + if (p.version != B20FactoryLib.B20_CREATE_PARAMS_VERSION) { + revert UnsupportedVersion(p.version, B20Variant(variant)); + } name_ = p.name; symbol_ = p.symbol; admin = p.initialAdmin; decimals = 18; - } else if (variant == B20Variant.STABLECOIN) { + } else if (B20Variant(variant) == B20Variant.STABLECOIN) { B20StablecoinCreateParams memory p = abi.decode(params, (B20StablecoinCreateParams)); if (p.version != B20FactoryLib.B20_STABLECOIN_CREATE_PARAMS_VERSION) { - revert UnsupportedVersion(p.version, variant); + revert UnsupportedVersion(p.version, B20Variant(variant)); } // Empty currency must be rejected explicitly: the format-check loop below has // no bytes to inspect on empty input and would vacuously succeed otherwise. @@ -133,10 +144,10 @@ contract MockB20Factory is IB20Factory { admin = p.initialAdmin; decimals = 6; currency_ = p.currency; - } else if (variant == B20Variant.SECURITY) { + } else if (B20Variant(variant) == B20Variant.SECURITY) { B20SecurityCreateParams memory p = abi.decode(params, (B20SecurityCreateParams)); if (p.version != B20FactoryLib.B20_SECURITY_CREATE_PARAMS_VERSION) { - revert UnsupportedVersion(p.version, variant); + revert UnsupportedVersion(p.version, B20Variant(variant)); } if (bytes(p.isin).length == 0) revert MissingRequiredField("isin"); name_ = p.name; @@ -150,13 +161,13 @@ contract MockB20Factory is IB20Factory { } // -- 2-3. Compute address; refuse to overwrite -- - token = _computeAddress(variant, msg.sender, salt); + token = _computeAddress(B20Variant(variant), msg.sender, salt); if (token.code.length != 0) revert TokenAlreadyExists(token); // -- 4. Etch the variant-appropriate runtime bytecode -- - if (variant == B20Variant.DEFAULT) { + if (B20Variant(variant) == B20Variant.DEFAULT) { vm.etch(token, type(MockB20).runtimeCode); - } else if (variant == B20Variant.STABLECOIN) { + } else if (B20Variant(variant) == B20Variant.STABLECOIN) { vm.etch(token, type(MockB20Stablecoin).runtimeCode); } else { // SECURITY (NONE / unknown variants already reverted above). @@ -168,16 +179,16 @@ contract MockB20Factory is IB20Factory { // canonical grantRole path in step 7 so RoleGranted fires // from the token. _writeBaseStorage(token, name_, symbol_); - if (variant == B20Variant.STABLECOIN) { + if (B20Variant(variant) == B20Variant.STABLECOIN) { _writeStablecoinStorage(token, currency_); - } else if (variant == B20Variant.SECURITY) { + } else if (B20Variant(variant) == B20Variant.SECURITY) { _writeSecurityStorage(token, isin_, minimumRedeemable_); } // -- 6. Emit B20Created. Identity-only signal; admin role // assignment is announced via the standard RoleGranted // event from step 7. - emit B20Created(token, variant, name_, symbol_, decimals); + emit B20Created(token, B20Variant(variant), name_, symbol_, decimals); // -- 7. Grant the initial admin role via the canonical path. // msg.sender at the token is address(this) == factory, @@ -225,8 +236,11 @@ contract MockB20Factory is IB20Factory { bytes32 internal constant DEFAULT_ADMIN_ROLE = bytes32(0); /// @inheritdoc IB20Factory - function getB20Address(B20Variant variant, address sender, bytes32 salt) external pure returns (address) { - return _computeAddress(variant, sender, salt); + function getB20Address(uint8 variant, address sender, bytes32 salt) external pure returns (address) { + // Mirror the Rust precompile: reject out-of-range variant bytes with the + // typed `InvalidVariant()` selector rather than a decode-time Panic(0x21). + if (variant > uint8(type(B20Variant).max)) revert InvalidVariant(); + return _computeAddress(B20Variant(variant), sender, salt); } /// @inheritdoc IB20Factory diff --git a/test/unit/B20Factory/createB20_revertOrder.t.sol b/test/unit/B20Factory/createB20_revertOrder.t.sol index 67f8cd14..b8450574 100644 --- a/test/unit/B20Factory/createB20_revertOrder.t.sol +++ b/test/unit/B20Factory/createB20_revertOrder.t.sol @@ -39,7 +39,7 @@ contract B20FactoryCreateB20RevertOrderTest is B20FactoryTest { IB20Factory.UnsupportedVersion.selector, badVersion, IB20Factory.B20Variant.STABLECOIN ) ); - factory.createB20(IB20Factory.B20Variant.STABLECOIN, salt, abi.encode(p), new bytes[](0)); + factory.createB20(uint8(IB20Factory.B20Variant.STABLECOIN), salt, abi.encode(p), new bytes[](0)); } /// @notice For the SECURITY arm: VERSION beats MISSING-ISIN. @@ -60,6 +60,6 @@ contract B20FactoryCreateB20RevertOrderTest is B20FactoryTest { vm.expectRevert( abi.encodeWithSelector(IB20Factory.UnsupportedVersion.selector, badVersion, IB20Factory.B20Variant.SECURITY) ); - factory.createB20(IB20Factory.B20Variant.SECURITY, salt, abi.encode(p), new bytes[](0)); + factory.createB20(uint8(IB20Factory.B20Variant.SECURITY), salt, abi.encode(p), new bytes[](0)); } } diff --git a/test/unit/B20Factory/createToken.t.sol b/test/unit/B20Factory/createToken.t.sol index ec0540b4..e1449b34 100644 --- a/test/unit/B20Factory/createToken.t.sol +++ b/test/unit/B20Factory/createToken.t.sol @@ -35,17 +35,18 @@ contract B20FactoryCreateB20Test is B20FactoryTest { //////////////////////////////////////////////////////////////*/ /// @notice Verifies createToken rejects raw variant bytes outside the B20Variant enum range - /// @dev B20Variant has no "NONE" sentinel; typed callers cannot construct an out-of-range - /// value. The ABI decoder rejects out-of-range enum bytes with a Panic(0x21) before the - /// factory body's `else { revert InvalidVariant(); }` branch is ever reached, so the - /// observable behavior from a raw-bytes caller is a decode-time panic rather than a - /// typed factory revert. + /// with the typed `InvalidVariant()` selector. + /// @dev The `variant` parameter is typed as `uint8` (not the enum), so any out-of-range + /// byte reaches the factory body and is rejected by the entry-point guard with + /// `InvalidVariant()`, matching the Rust precompile's selector exactly. Pinned via + /// `vm.expectRevert(selector)` so a silent regression to a decode-time `Panic(0x21)` + /// fails the test instead of passing under the bare-`vm.expectRevert()` "any revert" + /// semantics. function test_createB20_revert_outOfRangeVariant(address caller, bytes32 salt, uint8 badVariant) public { _assumeValidCaller(caller); badVariant = uint8(bound(uint256(badVariant), uint256(type(IB20Factory.B20Variant).max) + 1, 255)); vm.prank(caller); - // ABI decoder panic for out-of-range enum value. - vm.expectRevert(); + vm.expectRevert(IB20Factory.InvalidVariant.selector); (bool ok,) = address(factory) .call( abi.encodeWithSelector( @@ -66,7 +67,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { vm.expectRevert( abi.encodeWithSelector(IB20Factory.UnsupportedVersion.selector, badVersion, IB20Factory.B20Variant.DEFAULT) ); - factory.createB20(IB20Factory.B20Variant.DEFAULT, salt, abi.encode(p), new bytes[](0)); + factory.createB20(uint8(IB20Factory.B20Variant.DEFAULT), salt, abi.encode(p), new bytes[](0)); } /// @notice Verifies createToken reverts for any unsupported version byte on the STABLECOIN variant @@ -85,7 +86,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { IB20Factory.UnsupportedVersion.selector, badVersion, IB20Factory.B20Variant.STABLECOIN ) ); - factory.createB20(IB20Factory.B20Variant.STABLECOIN, salt, abi.encode(p), new bytes[](0)); + factory.createB20(uint8(IB20Factory.B20Variant.STABLECOIN), salt, abi.encode(p), new bytes[](0)); } // STABLECOIN currency validation: every byte must be an uppercase ASCII letter (A-Z). @@ -103,7 +104,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { IB20Factory.B20StablecoinCreateParams memory p = _stablecoinParams("Test", "TST", admin, code); vm.prank(caller); vm.expectRevert(abi.encodeWithSelector(IB20Factory.InvalidCurrency.selector, code)); - factory.createB20(IB20Factory.B20Variant.STABLECOIN, salt, abi.encode(p), new bytes[](0)); + factory.createB20(uint8(IB20Factory.B20Variant.STABLECOIN), salt, abi.encode(p), new bytes[](0)); } function _isValidFiatCode(string memory code) private pure returns (bool) { @@ -129,7 +130,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { vm.expectRevert( abi.encodeWithSelector(IB20Factory.UnsupportedVersion.selector, badVersion, IB20Factory.B20Variant.SECURITY) ); - factory.createB20(IB20Factory.B20Variant.SECURITY, salt, abi.encode(p), new bytes[](0)); + factory.createB20(uint8(IB20Factory.B20Variant.SECURITY), salt, abi.encode(p), new bytes[](0)); } /// @notice Verifies security createToken reverts when isin is the empty string @@ -139,7 +140,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { IB20Factory.B20SecurityCreateParams memory p = _securityParams("Security Test", "SEC", admin, "", 0); vm.prank(caller); vm.expectRevert(abi.encodeWithSelector(IB20Factory.MissingRequiredField.selector, "isin")); - factory.createB20(IB20Factory.B20Variant.SECURITY, salt, abi.encode(p), new bytes[](0)); + factory.createB20(uint8(IB20Factory.B20Variant.SECURITY), salt, abi.encode(p), new bytes[](0)); } /// @notice Verifies stablecoin createToken reverts when currency is the empty string @@ -151,7 +152,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { IB20Factory.B20StablecoinCreateParams memory p = _stablecoinParams("Stablecoin Test", "USD", admin, ""); vm.prank(caller); vm.expectRevert(abi.encodeWithSelector(IB20Factory.InvalidCurrency.selector, "")); - factory.createB20(IB20Factory.B20Variant.STABLECOIN, salt, abi.encode(p), new bytes[](0)); + factory.createB20(uint8(IB20Factory.B20Variant.STABLECOIN), salt, abi.encode(p), new bytes[](0)); } /// @notice Verifies createToken reverts when (variant, sender, salt) collides @@ -161,7 +162,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { address first = _createDefault(caller, salt, _b20Params(), new bytes[](0)); vm.prank(caller); vm.expectRevert(abi.encodeWithSelector(IB20Factory.TokenAlreadyExists.selector, first)); - factory.createB20(IB20Factory.B20Variant.DEFAULT, salt, abi.encode(_b20Params()), new bytes[](0)); + factory.createB20(uint8(IB20Factory.B20Variant.DEFAULT), salt, abi.encode(_b20Params()), new bytes[](0)); } /// @notice Verifies a failing initCall bubbles the underlying revert reason (regression test for L-01) @@ -177,7 +178,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { initCalls[0] = abi.encodeWithSelector(IB20.mint.selector, address(0), uint256(1)); vm.prank(caller); vm.expectRevert(abi.encodeWithSelector(IB20.InvalidReceiver.selector, address(0))); - factory.createB20(IB20Factory.B20Variant.DEFAULT, salt, abi.encode(_b20Params()), initCalls); + factory.createB20(uint8(IB20Factory.B20Variant.DEFAULT), salt, abi.encode(_b20Params()), initCalls); } /// @notice Verifies an empty-revert initCall is wrapped as InitCallFailed(index) (L-01 complement) @@ -193,7 +194,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { initCalls[0] = abi.encodeWithSelector(bytes4(0xdeadbeef)); vm.prank(caller); vm.expectRevert(abi.encodeWithSelector(IB20Factory.InitCallFailed.selector, uint256(0))); - factory.createB20(IB20Factory.B20Variant.DEFAULT, salt, abi.encode(_b20Params()), initCalls); + factory.createB20(uint8(IB20Factory.B20Variant.DEFAULT), salt, abi.encode(_b20Params()), initCalls); } /// @notice Verifies bubble + index for the SECOND init call (L-01 index correctness) @@ -210,7 +211,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { initCalls[1] = abi.encodeWithSelector(IB20.mint.selector, address(0), uint256(1)); vm.prank(caller); vm.expectRevert(abi.encodeWithSelector(IB20.InvalidReceiver.selector, address(0))); - factory.createB20(IB20Factory.B20Variant.DEFAULT, salt, abi.encode(_b20Params()), initCalls); + factory.createB20(uint8(IB20Factory.B20Variant.DEFAULT), salt, abi.encode(_b20Params()), initCalls); } /// @notice Verifies a failing initCall leaves the deterministic address empty @@ -222,14 +223,14 @@ contract B20FactoryCreateB20Test is B20FactoryTest { function test_createB20_revert_initCallFailed_revertsWholeCreation(address caller, bytes32 salt) public { _assumeValidCaller(caller); IB20Factory.B20CreateParams memory p = _b20Params(); - address predicted = factory.getB20Address(IB20Factory.B20Variant.DEFAULT, caller, salt); + address predicted = factory.getB20Address(uint8(IB20Factory.B20Variant.DEFAULT), caller, salt); bytes[] memory initCalls = new bytes[](1); initCalls[0] = abi.encodeWithSelector(IB20.mint.selector, address(0), uint256(1)); vm.prank(caller); vm.expectRevert(abi.encodeWithSelector(IB20.InvalidReceiver.selector, address(0))); - factory.createB20(IB20Factory.B20Variant.DEFAULT, salt, abi.encode(p), initCalls); + factory.createB20(uint8(IB20Factory.B20Variant.DEFAULT), salt, abi.encode(p), initCalls); // After the failed creation, the predicted address has no code, // and a fresh creation with the same args succeeds. @@ -247,7 +248,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { function test_createB20_success_defaultMatchesPrediction(address caller, bytes32 salt) public { _assumeValidCaller(caller); IB20Factory.B20CreateParams memory p = _b20Params("Test", "TST", admin); - address predicted = factory.getB20Address(IB20Factory.B20Variant.DEFAULT, caller, salt); + address predicted = factory.getB20Address(uint8(IB20Factory.B20Variant.DEFAULT), caller, salt); address actual = _createDefault(caller, salt, p, new bytes[](0)); assertEq(actual, predicted, "createToken address must match prediction"); } @@ -256,7 +257,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { /// @dev Address determinism: returned address must equal getTokenAddress(STABLECOIN, sender, salt) function test_createB20_success_stablecoinMatchesPrediction(address caller, bytes32 salt) public { _assumeValidCaller(caller); - address predicted = factory.getB20Address(IB20Factory.B20Variant.STABLECOIN, caller, salt); + address predicted = factory.getB20Address(uint8(IB20Factory.B20Variant.STABLECOIN), caller, salt); address actual = _createStablecoin(caller, salt, _stablecoinParams(), new bytes[](0)); assertEq(actual, predicted, "createToken address must match prediction"); } @@ -265,7 +266,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { /// @dev Address determinism: returned address must equal getTokenAddress(SECURITY, sender, salt) function test_createB20_success_securityMatchesPrediction(address caller, bytes32 salt) public { _assumeValidCaller(caller); - address predicted = factory.getB20Address(IB20Factory.B20Variant.SECURITY, caller, salt); + address predicted = factory.getB20Address(uint8(IB20Factory.B20Variant.SECURITY), caller, salt); address actual = _createSecurity(caller, salt, _securityParams(), new bytes[](0)); assertEq(actual, predicted, "createToken address must match prediction"); } @@ -465,7 +466,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { function test_createB20_success_emitsB20Created(address caller, bytes32 salt) public { _assumeValidCaller(caller); IB20Factory.B20CreateParams memory p = _b20Params("MyToken", "MYT", admin); - address predicted = factory.getB20Address(IB20Factory.B20Variant.DEFAULT, caller, salt); + address predicted = factory.getB20Address(uint8(IB20Factory.B20Variant.DEFAULT), caller, salt); vm.expectEmit(true, true, false, true, address(factory)); emit IB20Factory.B20Created(predicted, IB20Factory.B20Variant.DEFAULT, "MyToken", "MYT", 18); @@ -478,7 +479,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { 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); - address predicted = factory.getB20Address(IB20Factory.B20Variant.SECURITY, caller, salt); + address predicted = factory.getB20Address(uint8(IB20Factory.B20Variant.SECURITY), caller, salt); vm.expectEmit(true, true, false, true, address(factory)); emit IB20Factory.B20Created(predicted, IB20Factory.B20Variant.SECURITY, "Security Test", "SEC", 6); @@ -723,7 +724,7 @@ contract B20FactoryCreateB20Test is B20FactoryTest { uint160 expectedAddr = (uint160(0xB2) << 152) | (uint160(uint8(IB20Factory.B20Variant.DEFAULT)) << 72) | uint160(uint72(expectedTail)); - address actual = factory.getB20Address(IB20Factory.B20Variant.DEFAULT, sender, salt); + address actual = factory.getB20Address(uint8(IB20Factory.B20Variant.DEFAULT), sender, salt); assertEq(actual, address(expectedAddr), "factory must derive address via abi.encode of (sender, salt)"); } diff --git a/test/unit/B20Factory/getTokenAddress.t.sol b/test/unit/B20Factory/getTokenAddress.t.sol index b051d824..4fdb61d1 100644 --- a/test/unit/B20Factory/getTokenAddress.t.sol +++ b/test/unit/B20Factory/getTokenAddress.t.sol @@ -6,18 +6,18 @@ import {IB20Factory} from "src/interfaces/IB20Factory.sol"; import {B20FactoryTest} from "test/lib/B20FactoryTest.sol"; contract B20FactoryGetTokenAddressTest is B20FactoryTest { - /// @notice Wraps an arbitrary uint8 into a valid B20Variant ordinal. - /// @dev Bounds to the enum range (DEFAULT, STABLECOIN, SECURITY). The address derivation - /// 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)))); + /// @notice Wraps an arbitrary uint8 into a valid B20Variant ordinal (as `uint8`). + /// @dev Bounds to the enum range (DEFAULT=0, STABLECOIN=1, SECURITY=2). The factory + /// takes `uint8` on the wire (matching the Rust precompile); this helper hands + /// back a byte that is guaranteed to pass the entry-point range check. + function _boundVariant(uint8 variantInt) internal pure returns (uint8) { + return uint8(bound(uint256(variantInt), 0, uint256(type(IB20Factory.B20Variant).max))); } /// @notice Verifies getTokenAddress is deterministic for the same inputs /// @dev Pure view: repeated calls with identical args must return identical addresses function test_getB20Address_success_deterministic(uint8 variantInt, address sender, bytes32 salt) public view { - IB20Factory.B20Variant variant = _boundVariant(variantInt); + uint8 variant = _boundVariant(variantInt); address a = factory.getB20Address(variant, sender, salt); address b = factory.getB20Address(variant, sender, salt); assertEq(a, b, "address derivation must be deterministic"); @@ -26,9 +26,9 @@ contract B20FactoryGetTokenAddressTest is B20FactoryTest { /// @notice Verifies different variants produce different addresses for the same (sender, salt) /// @dev Variant byte at position [10] is part of the address derivation function test_getB20Address_success_differentVariantDiffers(address sender, bytes32 salt) public view { - address asDefault = factory.getB20Address(IB20Factory.B20Variant.DEFAULT, sender, salt); - address asStablecoin = factory.getB20Address(IB20Factory.B20Variant.STABLECOIN, sender, salt); - address asSecurity = factory.getB20Address(IB20Factory.B20Variant.SECURITY, sender, salt); + address asDefault = factory.getB20Address(uint8(IB20Factory.B20Variant.DEFAULT), sender, salt); + address asStablecoin = factory.getB20Address(uint8(IB20Factory.B20Variant.STABLECOIN), sender, salt); + address asSecurity = factory.getB20Address(uint8(IB20Factory.B20Variant.SECURITY), sender, salt); assertTrue(asDefault != asStablecoin, "DEFAULT vs STABLECOIN must differ"); assertTrue(asDefault != asSecurity, "DEFAULT vs SECURITY must differ"); assertTrue(asStablecoin != asSecurity, "STABLECOIN vs SECURITY must differ"); @@ -41,7 +41,7 @@ contract B20FactoryGetTokenAddressTest is B20FactoryTest { view { vm.assume(s1 != s2); - IB20Factory.B20Variant variant = _boundVariant(variantInt); + uint8 variant = _boundVariant(variantInt); address a = factory.getB20Address(variant, s1, salt); address b = factory.getB20Address(variant, s2, salt); assertTrue(a != b, "different senders must yield different addresses"); @@ -54,7 +54,7 @@ contract B20FactoryGetTokenAddressTest is B20FactoryTest { view { vm.assume(s1 != s2); - IB20Factory.B20Variant variant = _boundVariant(variantInt); + uint8 variant = _boundVariant(variantInt); address a = factory.getB20Address(variant, sender, s1); address b = factory.getB20Address(variant, sender, s2); assertTrue(a != b, "different salts must yield different addresses"); @@ -64,7 +64,7 @@ contract B20FactoryGetTokenAddressTest is B20FactoryTest { /// @dev Address schema: bytes [0:10] are the shared 0xB200...000 prefix /// (byte [0] = 0xB2, bytes [1:9] = 0x00) function test_getB20Address_success_prefixIsB20(uint8 variantInt, address sender, bytes32 salt) public view { - IB20Factory.B20Variant variant = _boundVariant(variantInt); + uint8 variant = _boundVariant(variantInt); address a = factory.getB20Address(variant, sender, salt); // Drop the bottom 10 bytes; what remains should be 0xB2 followed by 9 zero bytes. @@ -79,13 +79,13 @@ contract B20FactoryGetTokenAddressTest is B20FactoryTest { public view { - IB20Factory.B20Variant variant = _boundVariant(variantInt); + uint8 variant = _boundVariant(variantInt); address a = factory.getB20Address(variant, sender, salt); // Byte [10] = bits [72..79]. Mask after shift. // forge-lint: disable-next-line(unsafe-typecast) uint8 byteAt10 = uint8(uint160(a) >> 72); - assertEq(byteAt10, uint8(variant), "address byte [10] must equal variant ordinal"); + assertEq(byteAt10, variant, "address byte [10] must equal variant ordinal"); } /// @notice Pins the absolute numeric ordinals of B20Variant @@ -107,7 +107,7 @@ contract B20FactoryGetTokenAddressTest is B20FactoryTest { public view { - IB20Factory.B20Variant variant = _boundVariant(variantInt); + uint8 variant = _boundVariant(variantInt); address a = factory.getB20Address(variant, sender, salt); bytes9 tail = bytes9(keccak256(abi.encode(sender, salt))); @@ -119,16 +119,15 @@ contract B20FactoryGetTokenAddressTest is B20FactoryTest { } /// @notice Verifies getB20Address rejects raw variant bytes outside the B20Variant enum range - /// @dev Mirrors the createB20 raw-bytes test. B20Variant has no "NONE" sentinel; typed - /// callers cannot construct an out-of-range value, so this path is only reachable via - /// raw calldata. Solidity rejects via the ABI decoder with Panic(0x21); the Rust - /// precompile rejects with the typed `InvalidVariant()` revert. The observable - /// contract from a raw-bytes caller is "the call reverts" on both backends; the - /// specific selector differs because Solidity has no opportunity to run a function - /// body before the decoder rejects. + /// with the typed `InvalidVariant()` selector. + /// @dev The `variant` parameter is typed as `uint8` (not the enum), so any out-of-range + /// byte reaches the factory body and is rejected with `InvalidVariant()`, matching + /// the Rust precompile's selector exactly. Pinned via `vm.expectRevert(selector)` so + /// a silent regression to a decode-time `Panic(0x21)` fails the test instead of + /// passing under the bare-`vm.expectRevert()` "any revert" semantics. function test_getB20Address_revert_outOfRangeVariant(address sender, bytes32 salt, uint8 badVariant) public { badVariant = uint8(bound(uint256(badVariant), uint256(type(IB20Factory.B20Variant).max) + 1, 255)); - vm.expectRevert(); + vm.expectRevert(IB20Factory.InvalidVariant.selector); (bool ok,) = address(factory).call(abi.encodeWithSelector(IB20Factory.getB20Address.selector, badVariant, sender, salt)); ok; // silence unused warning; the revert is asserted via vm.expectRevert.