From c836b9d7c2374808123aa931a623228f99ac5825 Mon Sep 17 00:00:00 2001 From: taek Date: Mon, 2 Feb 2026 19:28:01 +0900 Subject: [PATCH 1/3] fix(ECDSA): add input validation and signer checks --- src/signers/ECDSASigner.sol | 14 ++++++++++++-- src/validators/ECDSAValidator.sol | 5 +++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/signers/ECDSASigner.sol b/src/signers/ECDSASigner.sol index 7d8c236..7193480 100644 --- a/src/signers/ECDSASigner.sol +++ b/src/signers/ECDSASigner.sol @@ -15,6 +15,9 @@ import { } from "src/types/Constants.sol"; contract ECDSASigner is SignerBase, IStatelessValidator, IStatelessValidatorWithSender { + error InvalidDataLength(); + error ZeroAddressSigner(); + mapping(bytes32 id => mapping(address wallet => address)) public signer; function isModuleType(uint256 typeID) external pure override(IModule, SignerBase) returns (bool) { @@ -38,6 +41,8 @@ contract ECDSASigner is SignerBase, IStatelessValidator, IStatelessValidatorWith returns (uint256) { address owner = signer[id][msg.sender]; + // Fail if signer is not installed (prevents matching with failed recovery) + if (owner == address(0)) return SIG_VALIDATION_FAILED_UINT; return _verifySignature(userOpHash, userOp.signature, owner) ? SIG_VALIDATION_SUCCESS_UINT : SIG_VALIDATION_FAILED_UINT; @@ -50,12 +55,17 @@ contract ECDSASigner is SignerBase, IStatelessValidator, IStatelessValidatorWith returns (bytes4) { address owner = signer[id][msg.sender]; + // Fail if signer is not installed (prevents matching with failed recovery) + if (owner == address(0)) return ERC1271_INVALID; return _verifySignature(hash, sig, owner) ? ERC1271_MAGICVALUE : ERC1271_INVALID; } function _signerOninstall(bytes32 id, bytes calldata _data) internal override { - require(signer[id][msg.sender] == address(0)); - signer[id][msg.sender] = address(bytes20(_data[0:20])); + require(signer[id][msg.sender] == address(0), "Already installed"); + if (_data.length != 20) revert InvalidDataLength(); + address signerAddr = address(bytes20(_data[0:20])); + if (signerAddr == address(0)) revert ZeroAddressSigner(); + signer[id][msg.sender] = signerAddr; } function _signerOnUninstall(bytes32 id, bytes calldata) internal override { diff --git a/src/validators/ECDSAValidator.sol b/src/validators/ECDSAValidator.sol index cf189ce..c2f8547 100644 --- a/src/validators/ECDSAValidator.sol +++ b/src/validators/ECDSAValidator.sol @@ -30,9 +30,14 @@ contract ECDSAValidator is IValidator, IHook, IStatelessValidator, IStatelessVal mapping(address => ECDSAValidatorStorage) public ecdsaValidatorStorage; + error InvalidDataLength(); + error ZeroAddressOwner(); + function onInstall(bytes calldata _data) external payable override { if (_isInitialized(msg.sender)) revert AlreadyInitialized(msg.sender); + if (_data.length != 20) revert InvalidDataLength(); address owner = address(bytes20(_data[0:20])); + if (owner == address(0)) revert ZeroAddressOwner(); ecdsaValidatorStorage[msg.sender].owner = owner; emit OwnerRegistered(msg.sender, owner); } From 9b030928c2c0e4a8521cd27a81f6395cae909e90 Mon Sep 17 00:00:00 2001 From: taek Date: Tue, 3 Feb 2026 01:40:51 +0900 Subject: [PATCH 2/3] btt for ecdsa --- test/btt/ECDSASigner.t.sol | 329 +++++++++++++++++++++++++++++ test/btt/ECDSASigner.tree | 49 +++++ test/btt/ECDSAValidator.t.sol | 381 ++++++++++++++++++++++++++++++++++ test/btt/ECDSAValidator.tree | 58 ++++++ 4 files changed, 817 insertions(+) create mode 100644 test/btt/ECDSASigner.t.sol create mode 100644 test/btt/ECDSASigner.tree create mode 100644 test/btt/ECDSAValidator.t.sol create mode 100644 test/btt/ECDSAValidator.tree diff --git a/test/btt/ECDSASigner.t.sol b/test/btt/ECDSASigner.t.sol new file mode 100644 index 0000000..1e51514 --- /dev/null +++ b/test/btt/ECDSASigner.t.sol @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {ECDSA} from "solady/utils/ECDSA.sol"; +import {ECDSASigner} from "src/signers/ECDSASigner.sol"; +import {ISigner} from "src/interfaces/IERC7579Modules.sol"; +import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOperation.sol"; +import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol"; +import {EntryPointLib} from "../utils/EntryPointLib.sol"; +import { + SIG_VALIDATION_SUCCESS_UINT, + SIG_VALIDATION_FAILED_UINT, + MODULE_TYPE_SIGNER, + MODULE_TYPE_STATELESS_VALIDATOR, + MODULE_TYPE_STATELESS_VALIDATOR_WITH_SENDER, + ERC1271_MAGICVALUE, + ERC1271_INVALID +} from "src/types/Constants.sol"; + +contract ECDSASignerBTTTest is Test { + ECDSASigner public ecdsaSigner; + IEntryPoint public ENTRYPOINT; + + address public owner; + uint256 public ownerKey; + address public wallet; + bytes32 public signerId; + + function setUp() public { + ecdsaSigner = new ECDSASigner(); + ENTRYPOINT = EntryPointLib.deploy(); + (owner, ownerKey) = makeAddrAndKey("owner"); + wallet = address(0x1234); + signerId = keccak256(abi.encodePacked("SIGNER_ID_1")); + } + + // ==================== Helper Functions ==================== + + function _installSigner() internal { + vm.prank(wallet); + ecdsaSigner.onInstall(abi.encodePacked(signerId, owner)); + } + + function _createUserOp() internal view returns (PackedUserOperation memory) { + return PackedUserOperation({ + sender: wallet, + nonce: 0, + initCode: "", + callData: "", + accountGasLimits: bytes32(abi.encodePacked(uint128(100000), uint128(200000))), + preVerificationGas: 0, + gasFees: bytes32(abi.encodePacked(uint128(1), uint128(1))), + paymasterAndData: "", + signature: "" + }); + } + + function _signRawHash(bytes32 hash, uint256 privateKey) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, hash); + return abi.encodePacked(r, s, v); + } + + function _signEthSignedMessageHash(bytes32 hash, uint256 privateKey) internal pure returns (bytes memory) { + bytes32 ethHash = ECDSA.toEthSignedMessageHash(hash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, ethHash); + return abi.encodePacked(r, s, v); + } + + // ==================== isModuleType Tests ==================== + + function test_WhenCallingIsModuleTypeWithMODULE_TYPE_SIGNER() external view { + // it should return true + bool result = ecdsaSigner.isModuleType(MODULE_TYPE_SIGNER); + assertTrue(result, "Should return true for MODULE_TYPE_SIGNER"); + } + + function test_WhenCallingIsModuleTypeWithMODULE_TYPE_STATELESS_VALIDATOR() external view { + // it should return true + bool result = ecdsaSigner.isModuleType(MODULE_TYPE_STATELESS_VALIDATOR); + assertTrue(result, "Should return true for MODULE_TYPE_STATELESS_VALIDATOR"); + } + + function test_WhenCallingIsModuleTypeWithMODULE_TYPE_STATELESS_VALIDATOR_WITH_SENDER() external view { + // it should return true + bool result = ecdsaSigner.isModuleType(MODULE_TYPE_STATELESS_VALIDATOR_WITH_SENDER); + assertTrue(result, "Should return true for MODULE_TYPE_STATELESS_VALIDATOR_WITH_SENDER"); + } + + function test_WhenCallingIsModuleTypeWithAnUnsupportedType() external view { + // it should return false + bool result = ecdsaSigner.isModuleType(999); + assertFalse(result, "Should return false for unsupported type"); + } + + // ==================== onInstall Tests ==================== + + function test_WhenCallingOnInstallWithInvalidDataLength() external { + // it should revert with InvalidDataLength + vm.startPrank(wallet); + // Data should be 32 bytes (signerId) + 20 bytes (address) = 52 bytes + // Send only 32 bytes (signerId) + 19 bytes = 51 bytes (invalid) + bytes memory invalidData = abi.encodePacked(signerId, bytes19(0)); + vm.expectRevert(ECDSASigner.InvalidDataLength.selector); + ecdsaSigner.onInstall(invalidData); + vm.stopPrank(); + } + + function test_WhenCallingOnInstallWithZeroAddressSigner() external { + // it should revert with ZeroAddressSigner + vm.startPrank(wallet); + bytes memory dataWithZeroAddress = abi.encodePacked(signerId, address(0)); + vm.expectRevert(ECDSASigner.ZeroAddressSigner.selector); + ecdsaSigner.onInstall(dataWithZeroAddress); + vm.stopPrank(); + } + + function test_WhenCallingOnInstallWithAlreadyInstalledSignerForSameId() external { + // it should revert with Already installed + _installSigner(); + + vm.startPrank(wallet); + vm.expectRevert("Already installed"); + ecdsaSigner.onInstall(abi.encodePacked(signerId, owner)); + vm.stopPrank(); + } + + function test_WhenCallingOnInstallWithValidData() external { + // it should store the signer address + _installSigner(); + + address storedSigner = ecdsaSigner.signer(signerId, wallet); + assertEq(storedSigner, owner, "Signer should be stored correctly"); + } + + // ==================== onUninstall Tests ==================== + + function test_RevertWhen_CallingOnUninstallWithSignerNotInstalled() external { + // it should revert + vm.startPrank(wallet); + vm.expectRevert(); + ecdsaSigner.onUninstall(abi.encodePacked(signerId, "")); + vm.stopPrank(); + } + + function test_WhenCallingOnUninstallWithSignerInstalled() external { + // it should delete the signer mapping + _installSigner(); + + vm.startPrank(wallet); + ecdsaSigner.onUninstall(abi.encodePacked(signerId, "")); + vm.stopPrank(); + + address storedSigner = ecdsaSigner.signer(signerId, wallet); + assertEq(storedSigner, address(0), "Signer should be deleted"); + } + + // ==================== checkUserOpSignature Tests ==================== + + function test_WhenCallingCheckUserOpSignatureWithSignerNotInstalled() external { + // it should return SIG_VALIDATION_FAILED_UINT + PackedUserOperation memory userOp = _createUserOp(); + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + userOp.signature = _signRawHash(userOpHash, ownerKey); + + vm.prank(wallet); + uint256 result = ecdsaSigner.checkUserOpSignature(signerId, userOp, userOpHash); + assertEq(result, SIG_VALIDATION_FAILED_UINT, "Should return SIG_VALIDATION_FAILED_UINT when signer not installed"); + } + + function test_WhenCallingCheckUserOpSignatureWithValidRawHashSignature() external { + // it should return SIG_VALIDATION_SUCCESS_UINT + _installSigner(); + + PackedUserOperation memory userOp = _createUserOp(); + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + userOp.signature = _signRawHash(userOpHash, ownerKey); + + vm.prank(wallet); + uint256 result = ecdsaSigner.checkUserOpSignature(signerId, userOp, userOpHash); + assertEq(result, SIG_VALIDATION_SUCCESS_UINT, "Should return SIG_VALIDATION_SUCCESS_UINT for valid raw hash signature"); + } + + function test_WhenCallingCheckUserOpSignatureWithValidEthSignedMessageHash() external { + // it should return SIG_VALIDATION_SUCCESS_UINT + _installSigner(); + + PackedUserOperation memory userOp = _createUserOp(); + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + userOp.signature = _signEthSignedMessageHash(userOpHash, ownerKey); + + vm.prank(wallet); + uint256 result = ecdsaSigner.checkUserOpSignature(signerId, userOp, userOpHash); + assertEq(result, SIG_VALIDATION_SUCCESS_UINT, "Should return SIG_VALIDATION_SUCCESS_UINT for valid eth signed message hash"); + } + + function test_WhenCallingCheckUserOpSignatureWithInvalidSignature() external { + // it should return SIG_VALIDATION_FAILED_UINT + _installSigner(); + + PackedUserOperation memory userOp = _createUserOp(); + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + // Sign with a different hash to create invalid signature + bytes32 wrongHash = keccak256(abi.encodePacked("wrong")); + userOp.signature = _signRawHash(wrongHash, ownerKey); + + vm.prank(wallet); + uint256 result = ecdsaSigner.checkUserOpSignature(signerId, userOp, userOpHash); + assertEq(result, SIG_VALIDATION_FAILED_UINT, "Should return SIG_VALIDATION_FAILED_UINT for invalid signature"); + } + + // ==================== checkSignature Tests ==================== + + function test_WhenCallingCheckSignatureWithSignerNotInstalled() external { + // it should return ERC1271_INVALID + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes memory sig = _signRawHash(testHash, ownerKey); + + vm.prank(wallet); + bytes4 result = ecdsaSigner.checkSignature(signerId, address(0), testHash, sig); + assertEq(result, ERC1271_INVALID, "Should return ERC1271_INVALID when signer not installed"); + } + + function test_WhenCallingCheckSignatureWithValidRawHashSignature() external { + // it should return ERC1271_MAGICVALUE + _installSigner(); + + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes memory sig = _signRawHash(testHash, ownerKey); + + vm.prank(wallet); + bytes4 result = ecdsaSigner.checkSignature(signerId, address(0), testHash, sig); + assertEq(result, ERC1271_MAGICVALUE, "Should return ERC1271_MAGICVALUE for valid raw hash signature"); + } + + function test_WhenCallingCheckSignatureWithValidEthSignedMessageHash() external { + // it should return ERC1271_MAGICVALUE + _installSigner(); + + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes memory sig = _signEthSignedMessageHash(testHash, ownerKey); + + vm.prank(wallet); + bytes4 result = ecdsaSigner.checkSignature(signerId, address(0), testHash, sig); + assertEq(result, ERC1271_MAGICVALUE, "Should return ERC1271_MAGICVALUE for valid eth signed message hash"); + } + + function test_WhenCallingCheckSignatureWithInvalidSignature() external { + // it should return ERC1271_INVALID + _installSigner(); + + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes32 wrongHash = keccak256(abi.encodePacked("WRONG_HASH")); + bytes memory sig = _signRawHash(wrongHash, ownerKey); + + vm.prank(wallet); + bytes4 result = ecdsaSigner.checkSignature(signerId, address(0), testHash, sig); + assertEq(result, ERC1271_INVALID, "Should return ERC1271_INVALID for invalid signature"); + } + + // ==================== validateSignatureWithData Tests ==================== + + function test_WhenCallingValidateSignatureWithDataWithValidRawHashSignature() external view { + // it should return true + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes memory sig = _signRawHash(testHash, ownerKey); + bytes memory data = abi.encodePacked(owner); + + bool result = ecdsaSigner.validateSignatureWithData(testHash, sig, data); + assertTrue(result, "Should return true for valid raw hash signature"); + } + + function test_WhenCallingValidateSignatureWithDataWithValidEthSignedMessageHash() external view { + // it should return true + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes memory sig = _signEthSignedMessageHash(testHash, ownerKey); + bytes memory data = abi.encodePacked(owner); + + bool result = ecdsaSigner.validateSignatureWithData(testHash, sig, data); + assertTrue(result, "Should return true for valid eth signed message hash"); + } + + function test_WhenCallingValidateSignatureWithDataWithInvalidSignature() external view { + // it should return false + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes32 wrongHash = keccak256(abi.encodePacked("WRONG_HASH")); + bytes memory sig = _signRawHash(wrongHash, ownerKey); + bytes memory data = abi.encodePacked(owner); + + bool result = ecdsaSigner.validateSignatureWithData(testHash, sig, data); + assertFalse(result, "Should return false for invalid signature"); + } + + // ==================== validateSignatureWithDataWithSender Tests ==================== + + function test_WhenCallingValidateSignatureWithDataWithSenderWithValidRawHashSignature() external view { + // it should return true + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes memory sig = _signRawHash(testHash, ownerKey); + bytes memory data = abi.encodePacked(owner); + address sender = address(0x5678); + + bool result = ecdsaSigner.validateSignatureWithDataWithSender(sender, testHash, sig, data); + assertTrue(result, "Should return true for valid raw hash signature"); + } + + function test_WhenCallingValidateSignatureWithDataWithSenderWithValidEthSignedMessageHash() external view { + // it should return true + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes memory sig = _signEthSignedMessageHash(testHash, ownerKey); + bytes memory data = abi.encodePacked(owner); + address sender = address(0x5678); + + bool result = ecdsaSigner.validateSignatureWithDataWithSender(sender, testHash, sig, data); + assertTrue(result, "Should return true for valid eth signed message hash"); + } + + function test_WhenCallingValidateSignatureWithDataWithSenderWithInvalidSignature() external view { + // it should return false + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes32 wrongHash = keccak256(abi.encodePacked("WRONG_HASH")); + bytes memory sig = _signRawHash(wrongHash, ownerKey); + bytes memory data = abi.encodePacked(owner); + address sender = address(0x5678); + + bool result = ecdsaSigner.validateSignatureWithDataWithSender(sender, testHash, sig, data); + assertFalse(result, "Should return false for invalid signature"); + } +} diff --git a/test/btt/ECDSASigner.tree b/test/btt/ECDSASigner.tree new file mode 100644 index 0000000..04b13bd --- /dev/null +++ b/test/btt/ECDSASigner.tree @@ -0,0 +1,49 @@ +ECDSASignerBTTTest +├── when calling isModuleType with MODULE_TYPE_SIGNER +│ └── it should return true +├── when calling isModuleType with MODULE_TYPE_STATELESS_VALIDATOR +│ └── it should return true +├── when calling isModuleType with MODULE_TYPE_STATELESS_VALIDATOR_WITH_SENDER +│ └── it should return true +├── when calling isModuleType with an unsupported type +│ └── it should return false +├── when calling onInstall with invalid data length +│ └── it should revert with InvalidDataLength +├── when calling onInstall with zero address signer +│ └── it should revert with ZeroAddressSigner +├── when calling onInstall with already installed signer for same id +│ └── it should revert with Already installed +├── when calling onInstall with valid data +│ └── it should store the signer address +├── when calling onUninstall with signer not installed +│ └── it should revert +├── when calling onUninstall with signer installed +│ └── it should delete the signer mapping +├── when calling checkUserOpSignature with signer not installed +│ └── it should return SIG_VALIDATION_FAILED_UINT +├── when calling checkUserOpSignature with valid raw hash signature +│ └── it should return SIG_VALIDATION_SUCCESS_UINT +├── when calling checkUserOpSignature with valid eth signed message hash +│ └── it should return SIG_VALIDATION_SUCCESS_UINT +├── when calling checkUserOpSignature with invalid signature +│ └── it should return SIG_VALIDATION_FAILED_UINT +├── when calling checkSignature with signer not installed +│ └── it should return ERC1271_INVALID +├── when calling checkSignature with valid raw hash signature +│ └── it should return ERC1271_MAGICVALUE +├── when calling checkSignature with valid eth signed message hash +│ └── it should return ERC1271_MAGICVALUE +├── when calling checkSignature with invalid signature +│ └── it should return ERC1271_INVALID +├── when calling validateSignatureWithData with valid raw hash signature +│ └── it should return true +├── when calling validateSignatureWithData with valid eth signed message hash +│ └── it should return true +├── when calling validateSignatureWithData with invalid signature +│ └── it should return false +├── when calling validateSignatureWithDataWithSender with valid raw hash signature +│ └── it should return true +├── when calling validateSignatureWithDataWithSender with valid eth signed message hash +│ └── it should return true +└── when calling validateSignatureWithDataWithSender with invalid signature + └── it should return false diff --git a/test/btt/ECDSAValidator.t.sol b/test/btt/ECDSAValidator.t.sol new file mode 100644 index 0000000..d4e776e --- /dev/null +++ b/test/btt/ECDSAValidator.t.sol @@ -0,0 +1,381 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {ECDSA} from "solady/utils/ECDSA.sol"; +import {ECDSAValidator} from "src/validators/ECDSAValidator.sol"; +import {IValidator, IHook, IModule} from "src/interfaces/IERC7579Modules.sol"; +import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOperation.sol"; +import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol"; +import {EntryPointLib} from "../utils/EntryPointLib.sol"; +import { + SIG_VALIDATION_SUCCESS_UINT, + SIG_VALIDATION_FAILED_UINT, + MODULE_TYPE_VALIDATOR, + MODULE_TYPE_HOOK, + MODULE_TYPE_STATELESS_VALIDATOR, + MODULE_TYPE_STATELESS_VALIDATOR_WITH_SENDER, + ERC1271_MAGICVALUE, + ERC1271_INVALID +} from "src/types/Constants.sol"; + +contract ECDSAValidatorBTTTest is Test { + ECDSAValidator public ecdsaValidator; + IEntryPoint public ENTRYPOINT; + + address public owner; + uint256 public ownerKey; + address public wallet; + + event OwnerRegistered(address indexed kernel, address indexed owner); + + function setUp() public { + ecdsaValidator = new ECDSAValidator(); + ENTRYPOINT = EntryPointLib.deploy(); + (owner, ownerKey) = makeAddrAndKey("owner"); + wallet = address(0x1234); + } + + // ==================== Helper Functions ==================== + + function _installValidator() internal { + vm.prank(wallet); + ecdsaValidator.onInstall(abi.encodePacked(owner)); + } + + function _createUserOp() internal view returns (PackedUserOperation memory) { + return PackedUserOperation({ + sender: wallet, + nonce: 0, + initCode: "", + callData: "", + accountGasLimits: bytes32(abi.encodePacked(uint128(100000), uint128(200000))), + preVerificationGas: 0, + gasFees: bytes32(abi.encodePacked(uint128(1), uint128(1))), + paymasterAndData: "", + signature: "" + }); + } + + function _signRawHash(bytes32 hash, uint256 privateKey) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, hash); + return abi.encodePacked(r, s, v); + } + + function _signEthSignedMessageHash(bytes32 hash, uint256 privateKey) internal pure returns (bytes memory) { + bytes32 ethHash = ECDSA.toEthSignedMessageHash(hash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, ethHash); + return abi.encodePacked(r, s, v); + } + + // ==================== isModuleType Tests ==================== + + function test_WhenCallingIsModuleTypeWithMODULE_TYPE_VALIDATOR() external view { + // it should return true + bool result = ecdsaValidator.isModuleType(MODULE_TYPE_VALIDATOR); + assertTrue(result, "Should return true for MODULE_TYPE_VALIDATOR"); + } + + function test_WhenCallingIsModuleTypeWithMODULE_TYPE_HOOK() external view { + // it should return true + bool result = ecdsaValidator.isModuleType(MODULE_TYPE_HOOK); + assertTrue(result, "Should return true for MODULE_TYPE_HOOK"); + } + + function test_WhenCallingIsModuleTypeWithMODULE_TYPE_STATELESS_VALIDATOR() external view { + // it should return true + bool result = ecdsaValidator.isModuleType(MODULE_TYPE_STATELESS_VALIDATOR); + assertTrue(result, "Should return true for MODULE_TYPE_STATELESS_VALIDATOR"); + } + + function test_WhenCallingIsModuleTypeWithMODULE_TYPE_STATELESS_VALIDATOR_WITH_SENDER() external view { + // it should return true + bool result = ecdsaValidator.isModuleType(MODULE_TYPE_STATELESS_VALIDATOR_WITH_SENDER); + assertTrue(result, "Should return true for MODULE_TYPE_STATELESS_VALIDATOR_WITH_SENDER"); + } + + function test_WhenCallingIsModuleTypeWithAnUnsupportedType() external view { + // it should return false + bool result = ecdsaValidator.isModuleType(999); + assertFalse(result, "Should return false for unsupported type"); + } + + // ==================== onInstall Tests ==================== + + function test_WhenCallingOnInstallWithAlreadyInitializedValidator() external { + // it should revert with AlreadyInitialized + _installValidator(); + + vm.startPrank(wallet); + vm.expectRevert(abi.encodeWithSelector(IModule.AlreadyInitialized.selector, wallet)); + ecdsaValidator.onInstall(abi.encodePacked(owner)); + vm.stopPrank(); + } + + function test_WhenCallingOnInstallWithInvalidDataLength() external { + // it should revert with InvalidDataLength + vm.startPrank(wallet); + // Data should be 20 bytes (address), send 19 bytes + bytes memory invalidData = bytes.concat(bytes19(0)); + vm.expectRevert(ECDSAValidator.InvalidDataLength.selector); + ecdsaValidator.onInstall(invalidData); + vm.stopPrank(); + } + + function test_WhenCallingOnInstallWithZeroAddressOwner() external { + // it should revert with ZeroAddressOwner + vm.startPrank(wallet); + bytes memory dataWithZeroAddress = abi.encodePacked(address(0)); + vm.expectRevert(ECDSAValidator.ZeroAddressOwner.selector); + ecdsaValidator.onInstall(dataWithZeroAddress); + vm.stopPrank(); + } + + function test_WhenCallingOnInstallWithValidData() external { + // it should store the owner address + // it should emit OwnerRegistered event + vm.startPrank(wallet); + + vm.expectEmit(true, true, false, false); + emit OwnerRegistered(wallet, owner); + + ecdsaValidator.onInstall(abi.encodePacked(owner)); + vm.stopPrank(); + + (address storedOwner) = ecdsaValidator.ecdsaValidatorStorage(wallet); + assertEq(storedOwner, owner, "Owner should be stored correctly"); + } + + // ==================== onUninstall Tests ==================== + + function test_WhenCallingOnUninstallWithNotInitializedValidator() external { + // it should revert with NotInitialized + vm.startPrank(wallet); + vm.expectRevert(abi.encodeWithSelector(IModule.NotInitialized.selector, wallet)); + ecdsaValidator.onUninstall(""); + vm.stopPrank(); + } + + function test_WhenCallingOnUninstallWithInitializedValidator() external { + // it should delete the owner from storage + _installValidator(); + + vm.startPrank(wallet); + ecdsaValidator.onUninstall(""); + vm.stopPrank(); + + (address storedOwner) = ecdsaValidator.ecdsaValidatorStorage(wallet); + assertEq(storedOwner, address(0), "Owner should be deleted"); + } + + // ==================== validateUserOp Tests ==================== + + function test_WhenCallingValidateUserOpWithOwnerNotSet() external { + // it should return SIG_VALIDATION_FAILED_UINT + // Note: When owner is not set (address(0)), the signature check will fail + // because ECDSA.tryRecoverCalldata will not return address(0) for any valid signature + PackedUserOperation memory userOp = _createUserOp(); + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + userOp.signature = _signRawHash(userOpHash, ownerKey); + + vm.prank(wallet); + uint256 result = ecdsaValidator.validateUserOp(userOp, userOpHash); + // When owner is address(0), the signature cannot match it + assertEq(result, SIG_VALIDATION_FAILED_UINT, "Should return SIG_VALIDATION_FAILED_UINT when owner not set"); + } + + function test_WhenCallingValidateUserOpWithValidRawHashSignature() external { + // it should return SIG_VALIDATION_SUCCESS_UINT + _installValidator(); + + PackedUserOperation memory userOp = _createUserOp(); + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + userOp.signature = _signRawHash(userOpHash, ownerKey); + + vm.prank(wallet); + uint256 result = ecdsaValidator.validateUserOp(userOp, userOpHash); + assertEq(result, SIG_VALIDATION_SUCCESS_UINT, "Should return SIG_VALIDATION_SUCCESS_UINT for valid raw hash signature"); + } + + function test_WhenCallingValidateUserOpWithValidEthSignedMessageHash() external { + // it should return SIG_VALIDATION_SUCCESS_UINT + _installValidator(); + + PackedUserOperation memory userOp = _createUserOp(); + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + userOp.signature = _signEthSignedMessageHash(userOpHash, ownerKey); + + vm.prank(wallet); + uint256 result = ecdsaValidator.validateUserOp(userOp, userOpHash); + assertEq(result, SIG_VALIDATION_SUCCESS_UINT, "Should return SIG_VALIDATION_SUCCESS_UINT for valid eth signed message hash"); + } + + function test_WhenCallingValidateUserOpWithInvalidSignature() external { + // it should return SIG_VALIDATION_FAILED_UINT + _installValidator(); + + PackedUserOperation memory userOp = _createUserOp(); + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + // Sign with a different hash to create invalid signature + bytes32 wrongHash = keccak256(abi.encodePacked("wrong")); + userOp.signature = _signRawHash(wrongHash, ownerKey); + + vm.prank(wallet); + uint256 result = ecdsaValidator.validateUserOp(userOp, userOpHash); + assertEq(result, SIG_VALIDATION_FAILED_UINT, "Should return SIG_VALIDATION_FAILED_UINT for invalid signature"); + } + + // ==================== isValidSignatureWithSender Tests ==================== + + function test_WhenCallingIsValidSignatureWithSenderWithOwnerNotSet() external { + // it should return ERC1271_INVALID + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes memory sig = _signRawHash(testHash, ownerKey); + + vm.prank(wallet); + bytes4 result = ecdsaValidator.isValidSignatureWithSender(address(0), testHash, sig); + // When owner is address(0), no signature will match + assertEq(result, ERC1271_INVALID, "Should return ERC1271_INVALID when owner not set"); + } + + function test_WhenCallingIsValidSignatureWithSenderWithValidRawHashSignature() external { + // it should return ERC1271_MAGICVALUE + _installValidator(); + + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes memory sig = _signRawHash(testHash, ownerKey); + + vm.prank(wallet); + bytes4 result = ecdsaValidator.isValidSignatureWithSender(address(0), testHash, sig); + assertEq(result, ERC1271_MAGICVALUE, "Should return ERC1271_MAGICVALUE for valid raw hash signature"); + } + + function test_WhenCallingIsValidSignatureWithSenderWithValidEthSignedMessageHash() external { + // it should return ERC1271_MAGICVALUE + _installValidator(); + + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes memory sig = _signEthSignedMessageHash(testHash, ownerKey); + + vm.prank(wallet); + bytes4 result = ecdsaValidator.isValidSignatureWithSender(address(0), testHash, sig); + assertEq(result, ERC1271_MAGICVALUE, "Should return ERC1271_MAGICVALUE for valid eth signed message hash"); + } + + function test_WhenCallingIsValidSignatureWithSenderWithInvalidSignature() external { + // it should return ERC1271_INVALID + _installValidator(); + + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes32 wrongHash = keccak256(abi.encodePacked("WRONG_HASH")); + bytes memory sig = _signRawHash(wrongHash, ownerKey); + + vm.prank(wallet); + bytes4 result = ecdsaValidator.isValidSignatureWithSender(address(0), testHash, sig); + assertEq(result, ERC1271_INVALID, "Should return ERC1271_INVALID for invalid signature"); + } + + // ==================== validateSignatureWithData Tests ==================== + + function test_WhenCallingValidateSignatureWithDataWithValidRawHashSignature() external view { + // it should return true + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes memory sig = _signRawHash(testHash, ownerKey); + bytes memory data = abi.encodePacked(owner); + + bool result = ecdsaValidator.validateSignatureWithData(testHash, sig, data); + assertTrue(result, "Should return true for valid raw hash signature"); + } + + function test_WhenCallingValidateSignatureWithDataWithValidEthSignedMessageHash() external view { + // it should return true + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes memory sig = _signEthSignedMessageHash(testHash, ownerKey); + bytes memory data = abi.encodePacked(owner); + + bool result = ecdsaValidator.validateSignatureWithData(testHash, sig, data); + assertTrue(result, "Should return true for valid eth signed message hash"); + } + + function test_WhenCallingValidateSignatureWithDataWithInvalidSignature() external view { + // it should return false + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes32 wrongHash = keccak256(abi.encodePacked("WRONG_HASH")); + bytes memory sig = _signRawHash(wrongHash, ownerKey); + bytes memory data = abi.encodePacked(owner); + + bool result = ecdsaValidator.validateSignatureWithData(testHash, sig, data); + assertFalse(result, "Should return false for invalid signature"); + } + + // ==================== validateSignatureWithDataWithSender Tests ==================== + + function test_WhenCallingValidateSignatureWithDataWithSenderWithValidRawHashSignature() external view { + // it should return true + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes memory sig = _signRawHash(testHash, ownerKey); + bytes memory data = abi.encodePacked(owner); + address sender = address(0x5678); + + bool result = ecdsaValidator.validateSignatureWithDataWithSender(sender, testHash, sig, data); + assertTrue(result, "Should return true for valid raw hash signature"); + } + + function test_WhenCallingValidateSignatureWithDataWithSenderWithValidEthSignedMessageHash() external view { + // it should return true + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes memory sig = _signEthSignedMessageHash(testHash, ownerKey); + bytes memory data = abi.encodePacked(owner); + address sender = address(0x5678); + + bool result = ecdsaValidator.validateSignatureWithDataWithSender(sender, testHash, sig, data); + assertTrue(result, "Should return true for valid eth signed message hash"); + } + + function test_WhenCallingValidateSignatureWithDataWithSenderWithInvalidSignature() external view { + // it should return false + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + bytes32 wrongHash = keccak256(abi.encodePacked("WRONG_HASH")); + bytes memory sig = _signRawHash(wrongHash, ownerKey); + bytes memory data = abi.encodePacked(owner); + address sender = address(0x5678); + + bool result = ecdsaValidator.validateSignatureWithDataWithSender(sender, testHash, sig, data); + assertFalse(result, "Should return false for invalid signature"); + } + + // ==================== preCheck Tests ==================== + + function test_WhenCallingPreCheckWithMsgSenderNotTheOwner() external { + // it should revert with sender is not owner + _installValidator(); + + address notOwner = address(0x9999); + + vm.startPrank(wallet); + vm.expectRevert("ECDSAValidator: sender is not owner"); + ecdsaValidator.preCheck(notOwner, 0, ""); + vm.stopPrank(); + } + + function test_WhenCallingPreCheckWithMsgSenderAsTheOwner() external { + // it should return empty bytes + _installValidator(); + + vm.prank(wallet); + bytes memory result = ecdsaValidator.preCheck(owner, 0, ""); + assertEq(result, hex"", "Should return empty bytes"); + } + + // ==================== postCheck Tests ==================== + + function test_WhenCallingPostCheckWithAnyHookData() external { + // it should complete without reverting + // postCheck is a no-op function that should never revert + ecdsaValidator.postCheck(hex"1234"); + ecdsaValidator.postCheck(""); + ecdsaValidator.postCheck(abi.encodePacked("some data")); + // If we reach here without reverting, the test passes + assertTrue(true, "postCheck should complete without reverting"); + } +} diff --git a/test/btt/ECDSAValidator.tree b/test/btt/ECDSAValidator.tree new file mode 100644 index 0000000..36e49e6 --- /dev/null +++ b/test/btt/ECDSAValidator.tree @@ -0,0 +1,58 @@ +ECDSAValidatorBTTTest +├── when calling isModuleType with MODULE_TYPE_VALIDATOR +│ └── it should return true +├── when calling isModuleType with MODULE_TYPE_HOOK +│ └── it should return true +├── when calling isModuleType with MODULE_TYPE_STATELESS_VALIDATOR +│ └── it should return true +├── when calling isModuleType with MODULE_TYPE_STATELESS_VALIDATOR_WITH_SENDER +│ └── it should return true +├── when calling isModuleType with an unsupported type +│ └── it should return false +├── when calling onInstall with already initialized validator +│ └── it should revert with AlreadyInitialized +├── when calling onInstall with invalid data length +│ └── it should revert with InvalidDataLength +├── when calling onInstall with zero address owner +│ └── it should revert with ZeroAddressOwner +├── when calling onInstall with valid data +│ ├── it should store the owner address +│ └── it should emit OwnerRegistered event +├── when calling onUninstall with not initialized validator +│ └── it should revert with NotInitialized +├── when calling onUninstall with initialized validator +│ └── it should delete the owner from storage +├── when calling validateUserOp with owner not set +│ └── it should return SIG_VALIDATION_FAILED_UINT +├── when calling validateUserOp with valid raw hash signature +│ └── it should return SIG_VALIDATION_SUCCESS_UINT +├── when calling validateUserOp with valid eth signed message hash +│ └── it should return SIG_VALIDATION_SUCCESS_UINT +├── when calling validateUserOp with invalid signature +│ └── it should return SIG_VALIDATION_FAILED_UINT +├── when calling isValidSignatureWithSender with owner not set +│ └── it should return ERC1271_INVALID +├── when calling isValidSignatureWithSender with valid raw hash signature +│ └── it should return ERC1271_MAGICVALUE +├── when calling isValidSignatureWithSender with valid eth signed message hash +│ └── it should return ERC1271_MAGICVALUE +├── when calling isValidSignatureWithSender with invalid signature +│ └── it should return ERC1271_INVALID +├── when calling validateSignatureWithData with valid raw hash signature +│ └── it should return true +├── when calling validateSignatureWithData with valid eth signed message hash +│ └── it should return true +├── when calling validateSignatureWithData with invalid signature +│ └── it should return false +├── when calling validateSignatureWithDataWithSender with valid raw hash signature +│ └── it should return true +├── when calling validateSignatureWithDataWithSender with valid eth signed message hash +│ └── it should return true +├── when calling validateSignatureWithDataWithSender with invalid signature +│ └── it should return false +├── when calling preCheck with msgSender not the owner +│ └── it should revert with sender is not owner +├── when calling preCheck with msgSender as the owner +│ └── it should return empty bytes +└── when calling postCheck with any hookData + └── it should complete without reverting From fea23def7915f530d3fde33ab83c57d754317388 Mon Sep 17 00:00:00 2001 From: taek Date: Tue, 3 Feb 2026 01:45:20 +0900 Subject: [PATCH 3/3] btt --- test/btt/ECDSASigner.t.sol | 123 +++++++++++++++++++++++++++++++++- test/btt/ECDSASigner.tree | 16 ++++- test/btt/ECDSAValidator.t.sol | 115 ++++++++++++++++++++++++++++++- test/btt/ECDSAValidator.tree | 16 ++++- 4 files changed, 261 insertions(+), 9 deletions(-) diff --git a/test/btt/ECDSASigner.t.sol b/test/btt/ECDSASigner.t.sol index 1e51514..dfde846 100644 --- a/test/btt/ECDSASigner.t.sol +++ b/test/btt/ECDSASigner.t.sol @@ -165,7 +165,9 @@ contract ECDSASignerBTTTest is Test { vm.prank(wallet); uint256 result = ecdsaSigner.checkUserOpSignature(signerId, userOp, userOpHash); - assertEq(result, SIG_VALIDATION_FAILED_UINT, "Should return SIG_VALIDATION_FAILED_UINT when signer not installed"); + assertEq( + result, SIG_VALIDATION_FAILED_UINT, "Should return SIG_VALIDATION_FAILED_UINT when signer not installed" + ); } function test_WhenCallingCheckUserOpSignatureWithValidRawHashSignature() external { @@ -178,7 +180,11 @@ contract ECDSASignerBTTTest is Test { vm.prank(wallet); uint256 result = ecdsaSigner.checkUserOpSignature(signerId, userOp, userOpHash); - assertEq(result, SIG_VALIDATION_SUCCESS_UINT, "Should return SIG_VALIDATION_SUCCESS_UINT for valid raw hash signature"); + assertEq( + result, + SIG_VALIDATION_SUCCESS_UINT, + "Should return SIG_VALIDATION_SUCCESS_UINT for valid raw hash signature" + ); } function test_WhenCallingCheckUserOpSignatureWithValidEthSignedMessageHash() external { @@ -191,7 +197,11 @@ contract ECDSASignerBTTTest is Test { vm.prank(wallet); uint256 result = ecdsaSigner.checkUserOpSignature(signerId, userOp, userOpHash); - assertEq(result, SIG_VALIDATION_SUCCESS_UINT, "Should return SIG_VALIDATION_SUCCESS_UINT for valid eth signed message hash"); + assertEq( + result, + SIG_VALIDATION_SUCCESS_UINT, + "Should return SIG_VALIDATION_SUCCESS_UINT for valid eth signed message hash" + ); } function test_WhenCallingCheckUserOpSignatureWithInvalidSignature() external { @@ -326,4 +336,111 @@ contract ECDSASignerBTTTest is Test { bool result = ecdsaSigner.validateSignatureWithDataWithSender(sender, testHash, sig, data); assertFalse(result, "Should return false for invalid signature"); } + + function test_WhenCallingCheckUserOpSignatureWithSignerInstalledAndSignatureReturnsSuccessViaFirstBranch() + external + { + // it should return SIG_VALIDATION_SUCCESS_UINT via raw hash match + // This tests the first branch: if (_signer == ECDSA.tryRecoverCalldata(hash, sig)) return true; + _installSigner(); + + PackedUserOperation memory userOp = _createUserOp(); + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + // Sign the raw hash directly - this should match in the first branch + userOp.signature = _signRawHash(userOpHash, ownerKey); + + vm.prank(wallet); + uint256 result = ecdsaSigner.checkUserOpSignature(signerId, userOp, userOpHash); + assertEq( + result, + SIG_VALIDATION_SUCCESS_UINT, + "Should return SIG_VALIDATION_SUCCESS_UINT via raw hash match (first branch)" + ); + } + + function test_WhenCallingCheckUserOpSignatureWithSignerInstalledAndSignatureReturnsSuccessViaSecondBranch() + external + { + // it should return SIG_VALIDATION_SUCCESS_UINT via eth hash match + // This tests the second branch: first branch fails, then checks eth signed message hash + _installSigner(); + + PackedUserOperation memory userOp = _createUserOp(); + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + // Sign the eth signed message hash - first branch will fail, second branch will succeed + userOp.signature = _signEthSignedMessageHash(userOpHash, ownerKey); + + vm.prank(wallet); + uint256 result = ecdsaSigner.checkUserOpSignature(signerId, userOp, userOpHash); + assertEq( + result, + SIG_VALIDATION_SUCCESS_UINT, + "Should return SIG_VALIDATION_SUCCESS_UINT via eth hash match (second branch)" + ); + } + + function test_WhenCallingCheckUserOpSignatureWithSignerInstalledAndSignatureFailsBothBranches() external { + // it should return SIG_VALIDATION_FAILED_UINT after checking both hashes + // This tests when both branches fail: raw hash doesn't match AND eth signed hash doesn't match + _installSigner(); + + PackedUserOperation memory userOp = _createUserOp(); + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + + // Create a different keypair to sign - the recovered address won't match owner + (, uint256 differentKey) = makeAddrAndKey("different"); + userOp.signature = _signRawHash(userOpHash, differentKey); + + vm.prank(wallet); + uint256 result = ecdsaSigner.checkUserOpSignature(signerId, userOp, userOpHash); + assertEq( + result, + SIG_VALIDATION_FAILED_UINT, + "Should return SIG_VALIDATION_FAILED_UINT after both branches fail" + ); + } + + function test_WhenCallingCheckSignatureWithSignerInstalledAndSignatureReturnsSuccessViaFirstBranch() external { + // it should return ERC1271_MAGICVALUE via raw hash match + // This tests the first branch: if (_signer == ECDSA.tryRecoverCalldata(hash, sig)) return true; + _installSigner(); + + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + // Sign the raw hash directly - this should match in the first branch + bytes memory sig = _signRawHash(testHash, ownerKey); + + vm.prank(wallet); + bytes4 result = ecdsaSigner.checkSignature(signerId, address(0), testHash, sig); + assertEq(result, ERC1271_MAGICVALUE, "Should return ERC1271_MAGICVALUE via raw hash match (first branch)"); + } + + function test_WhenCallingCheckSignatureWithSignerInstalledAndSignatureReturnsSuccessViaSecondBranch() external { + // it should return ERC1271_MAGICVALUE via eth hash match + // This tests the second branch: first branch fails, then checks eth signed message hash + _installSigner(); + + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + // Sign the eth signed message hash - first branch will fail, second branch will succeed + bytes memory sig = _signEthSignedMessageHash(testHash, ownerKey); + + vm.prank(wallet); + bytes4 result = ecdsaSigner.checkSignature(signerId, address(0), testHash, sig); + assertEq(result, ERC1271_MAGICVALUE, "Should return ERC1271_MAGICVALUE via eth hash match (second branch)"); + } + + function test_WhenCallingCheckSignatureWithSignerInstalledAndSignatureFailsBothBranches() external { + // it should return ERC1271_INVALID after checking both hashes + // This tests when both branches fail: raw hash doesn't match AND eth signed hash doesn't match + _installSigner(); + + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + + // Create a different keypair to sign - the recovered address won't match owner + (, uint256 differentKey) = makeAddrAndKey("different"); + bytes memory sig = _signRawHash(testHash, differentKey); + + vm.prank(wallet); + bytes4 result = ecdsaSigner.checkSignature(signerId, address(0), testHash, sig); + assertEq(result, ERC1271_INVALID, "Should return ERC1271_INVALID after both branches fail"); + } } diff --git a/test/btt/ECDSASigner.tree b/test/btt/ECDSASigner.tree index 04b13bd..8a51ed6 100644 --- a/test/btt/ECDSASigner.tree +++ b/test/btt/ECDSASigner.tree @@ -45,5 +45,17 @@ ECDSASignerBTTTest │ └── it should return true ├── when calling validateSignatureWithDataWithSender with valid eth signed message hash │ └── it should return true -└── when calling validateSignatureWithDataWithSender with invalid signature - └── it should return false +├── when calling validateSignatureWithDataWithSender with invalid signature +│ └── it should return false +├── when calling checkUserOpSignature with signer installed and signature returns success via first branch +│ └── it should return SIG_VALIDATION_SUCCESS_UINT via raw hash match +├── when calling checkUserOpSignature with signer installed and signature returns success via second branch +│ └── it should return SIG_VALIDATION_SUCCESS_UINT via eth hash match +├── when calling checkUserOpSignature with signer installed and signature fails both branches +│ └── it should return SIG_VALIDATION_FAILED_UINT after checking both hashes +├── when calling checkSignature with signer installed and signature returns success via first branch +│ └── it should return ERC1271_MAGICVALUE via raw hash match +├── when calling checkSignature with signer installed and signature returns success via second branch +│ └── it should return ERC1271_MAGICVALUE via eth hash match +└── when calling checkSignature with signer installed and signature fails both branches + └── it should return ERC1271_INVALID after checking both hashes diff --git a/test/btt/ECDSAValidator.t.sol b/test/btt/ECDSAValidator.t.sol index d4e776e..286dfd9 100644 --- a/test/btt/ECDSAValidator.t.sol +++ b/test/btt/ECDSAValidator.t.sol @@ -194,7 +194,11 @@ contract ECDSAValidatorBTTTest is Test { vm.prank(wallet); uint256 result = ecdsaValidator.validateUserOp(userOp, userOpHash); - assertEq(result, SIG_VALIDATION_SUCCESS_UINT, "Should return SIG_VALIDATION_SUCCESS_UINT for valid raw hash signature"); + assertEq( + result, + SIG_VALIDATION_SUCCESS_UINT, + "Should return SIG_VALIDATION_SUCCESS_UINT for valid raw hash signature" + ); } function test_WhenCallingValidateUserOpWithValidEthSignedMessageHash() external { @@ -207,7 +211,11 @@ contract ECDSAValidatorBTTTest is Test { vm.prank(wallet); uint256 result = ecdsaValidator.validateUserOp(userOp, userOpHash); - assertEq(result, SIG_VALIDATION_SUCCESS_UINT, "Should return SIG_VALIDATION_SUCCESS_UINT for valid eth signed message hash"); + assertEq( + result, + SIG_VALIDATION_SUCCESS_UINT, + "Should return SIG_VALIDATION_SUCCESS_UINT for valid eth signed message hash" + ); } function test_WhenCallingValidateUserOpWithInvalidSignature() external { @@ -378,4 +386,107 @@ contract ECDSAValidatorBTTTest is Test { // If we reach here without reverting, the test passes assertTrue(true, "postCheck should complete without reverting"); } + + function test_WhenCallingValidateUserOpWithSignatureMatchingViaFirstBranch() external { + // it should return SIG_VALIDATION_SUCCESS_UINT via raw hash match + // This tests the first branch: if (signer == ECDSA.tryRecoverCalldata(hash, sig)) return true; + _installValidator(); + + PackedUserOperation memory userOp = _createUserOp(); + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + // Sign the raw hash directly - this should match in the first branch + userOp.signature = _signRawHash(userOpHash, ownerKey); + + vm.prank(wallet); + uint256 result = ecdsaValidator.validateUserOp(userOp, userOpHash); + assertEq( + result, + SIG_VALIDATION_SUCCESS_UINT, + "Should return SIG_VALIDATION_SUCCESS_UINT via raw hash match (first branch)" + ); + } + + function test_WhenCallingValidateUserOpWithSignatureMatchingViaSecondBranch() external { + // it should return SIG_VALIDATION_SUCCESS_UINT via eth hash match + // This tests the second branch: first branch fails, then checks eth signed message hash + _installValidator(); + + PackedUserOperation memory userOp = _createUserOp(); + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + // Sign the eth signed message hash - first branch will fail, second branch will succeed + userOp.signature = _signEthSignedMessageHash(userOpHash, ownerKey); + + vm.prank(wallet); + uint256 result = ecdsaValidator.validateUserOp(userOp, userOpHash); + assertEq( + result, + SIG_VALIDATION_SUCCESS_UINT, + "Should return SIG_VALIDATION_SUCCESS_UINT via eth hash match (second branch)" + ); + } + + function test_WhenCallingValidateUserOpWithSignatureFailingBothBranches() external { + // it should return SIG_VALIDATION_FAILED_UINT after checking both hashes + // This tests when both branches fail: raw hash doesn't match AND eth signed hash doesn't match + _installValidator(); + + PackedUserOperation memory userOp = _createUserOp(); + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + + // Create a different keypair to sign - the recovered address won't match owner + (, uint256 differentKey) = makeAddrAndKey("different"); + userOp.signature = _signRawHash(userOpHash, differentKey); + + vm.prank(wallet); + uint256 result = ecdsaValidator.validateUserOp(userOp, userOpHash); + assertEq( + result, + SIG_VALIDATION_FAILED_UINT, + "Should return SIG_VALIDATION_FAILED_UINT after both branches fail" + ); + } + + function test_WhenCallingIsValidSignatureWithSenderWithSignatureMatchingViaFirstBranch() external { + // it should return ERC1271_MAGICVALUE via raw hash match + // This tests the first branch: if (signer == ECDSA.tryRecoverCalldata(hash, sig)) return true; + _installValidator(); + + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + // Sign the raw hash directly - this should match in the first branch + bytes memory sig = _signRawHash(testHash, ownerKey); + + vm.prank(wallet); + bytes4 result = ecdsaValidator.isValidSignatureWithSender(address(0), testHash, sig); + assertEq(result, ERC1271_MAGICVALUE, "Should return ERC1271_MAGICVALUE via raw hash match (first branch)"); + } + + function test_WhenCallingIsValidSignatureWithSenderWithSignatureMatchingViaSecondBranch() external { + // it should return ERC1271_MAGICVALUE via eth hash match + // This tests the second branch: first branch fails, then checks eth signed message hash + _installValidator(); + + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + // Sign the eth signed message hash - first branch will fail, second branch will succeed + bytes memory sig = _signEthSignedMessageHash(testHash, ownerKey); + + vm.prank(wallet); + bytes4 result = ecdsaValidator.isValidSignatureWithSender(address(0), testHash, sig); + assertEq(result, ERC1271_MAGICVALUE, "Should return ERC1271_MAGICVALUE via eth hash match (second branch)"); + } + + function test_WhenCallingIsValidSignatureWithSenderWithSignatureFailingBothBranches() external { + // it should return ERC1271_INVALID after checking both hashes + // This tests when both branches fail: raw hash doesn't match AND eth signed hash doesn't match + _installValidator(); + + bytes32 testHash = keccak256(abi.encodePacked("TEST_HASH")); + + // Create a different keypair to sign - the recovered address won't match owner + (, uint256 differentKey) = makeAddrAndKey("different"); + bytes memory sig = _signRawHash(testHash, differentKey); + + vm.prank(wallet); + bytes4 result = ecdsaValidator.isValidSignatureWithSender(address(0), testHash, sig); + assertEq(result, ERC1271_INVALID, "Should return ERC1271_INVALID after both branches fail"); + } } diff --git a/test/btt/ECDSAValidator.tree b/test/btt/ECDSAValidator.tree index 36e49e6..32a1f42 100644 --- a/test/btt/ECDSAValidator.tree +++ b/test/btt/ECDSAValidator.tree @@ -54,5 +54,17 @@ ECDSAValidatorBTTTest │ └── it should revert with sender is not owner ├── when calling preCheck with msgSender as the owner │ └── it should return empty bytes -└── when calling postCheck with any hookData - └── it should complete without reverting +├── when calling postCheck with any hookData +│ └── it should complete without reverting +├── when calling validateUserOp with signature matching via first branch +│ └── it should return SIG_VALIDATION_SUCCESS_UINT via raw hash match +├── when calling validateUserOp with signature matching via second branch +│ └── it should return SIG_VALIDATION_SUCCESS_UINT via eth hash match +├── when calling validateUserOp with signature failing both branches +│ └── it should return SIG_VALIDATION_FAILED_UINT after checking both hashes +├── when calling isValidSignatureWithSender with signature matching via first branch +│ └── it should return ERC1271_MAGICVALUE via raw hash match +├── when calling isValidSignatureWithSender with signature matching via second branch +│ └── it should return ERC1271_MAGICVALUE via eth hash match +└── when calling isValidSignatureWithSender with signature failing both branches + └── it should return ERC1271_INVALID after checking both hashes