From eb1f4e14efa611ac250a353f3b640843dbfbaec9 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Mon, 23 Feb 2026 12:48:04 +0100 Subject: [PATCH 1/6] build(contracts/lib): update openzeppelin to v5.5.0 --- contracts/foundry.lock | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 contracts/foundry.lock diff --git a/contracts/foundry.lock b/contracts/foundry.lock new file mode 100644 index 000000000..e37d527bb --- /dev/null +++ b/contracts/foundry.lock @@ -0,0 +1,11 @@ +{ + "lib/forge-std": { + "rev": "1801b0541f4fda118a10798fd3486bb7051c5dd6" + }, + "lib/openzeppelin-contracts": { + "tag": { + "name": "v5.5.0", + "rev": "fcbae5394ae8ad52d8e580a3477db99814b9d565" + } + } +} \ No newline at end of file From b5cfc0341cacbca4460233b331d883330e12bc54 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Mon, 23 Feb 2026 12:48:29 +0100 Subject: [PATCH 2/6] docs(contracts): add SmartWalletValidator --- contracts/signature-validators.md | 38 ++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/contracts/signature-validators.md b/contracts/signature-validators.md index 408da7874..c219466ec 100644 --- a/contracts/signature-validators.md +++ b/contracts/signature-validators.md @@ -162,13 +162,41 @@ Default validator supporting standard ECDSA signatures. Automatically tries both --- +## SmartWalletValidator + +**Location:** `src/sigValidators/SmartWalletValidator.sol` + +### Description + +Validator supporting smart contract wallet signatures through ERC-4337 account abstraction standards. Enables contract-based accounts to sign states. + +### Signature Format + +Variable length signature that depends on the smart contract wallet's implementation. The validator handles both deployed and undeployed contracts. + +### Validation Logic + +1. Try ERC-6492 validation first (supports undeployed contracts via counterfactual verification) +2. If contract is deployed, use ERC-1271 validation (`isValidSignature` method) +3. Return `VALIDATION_SUCCESS` if signature is valid according to the contract's logic, `VALIDATION_FAILURE` otherwise + +### Use Cases + +- Smart contract wallets (Gnosis Safe, Argent, etc.) +- ERC-4337 account abstraction wallets +- Multi-signature contract accounts +- Social recovery wallets +- Any contract implementing ERC-1271 + +--- + ## SessionKeyValidator **Location:** `src/sigValidators/SessionKeyValidator.sol` ### Description -Enables delegation to temporary session keys. Useful for hot wallets, time-limited access, and gasless transactions. +Enables delegation to temporary session keys. Useful for hot wallets, time-limited access, and gasless transactions. Supports both EOA and smart contract wallet signatures through ERC-4337 (via ERC-1271 and ERC-6492). ### Signature Format @@ -176,7 +204,7 @@ Enables delegation to temporary session keys. Useful for hot wallets, time-limit struct SessionKeyAuthorization { address sessionKey; // Delegated signer bytes32 metadataHash; // Hashed expiration, permissions, etc. - bytes authSignature; // Participant's authorization (65 bytes) + bytes authSignature; // Participant's authorization signature } bytes sigBody = abi.encode(SessionKeyAuthorization, bytes sessionKeySignature) @@ -189,7 +217,11 @@ bytes sigBody = abi.encode(SessionKeyAuthorization, bytes sessionKeySignature) 1. Participant authorized the session key: `authData = abi.encode(sessionKey, metadataHash)` 2. Session key signed the state -Both use EIP-191 first, then raw ECDSA if that fails. +Both authorization and session key signatures support multiple formats: +- EIP-191 (standard wallet signatures) +- Raw ECDSA (tried if EIP-191 fails) +- ERC-1271 (smart contract wallet signatures) +- ERC-6492 (undeployed contract signatures) ### Metadata From 849fa5ef559bb8939b8a0f2582854977b248a68d Mon Sep 17 00:00:00 2001 From: nksazonov Date: Mon, 23 Feb 2026 12:48:38 +0100 Subject: [PATCH 3/6] feat(contracts): add SmartWalletValidator --- contracts/src/ChannelHub.sol | 6 +- .../src/interfaces/ISignatureValidator.sol | 3 +- .../sigValidators/SmartWalletValidator.sol | 46 +++ .../src/sigValidators/SwSignatureUtils.sol | 125 ++++++ .../test/sigValidators/SwSignatureUtils.t.sol | 375 ++++++++++++++++++ 5 files changed, 551 insertions(+), 4 deletions(-) create mode 100644 contracts/src/sigValidators/SmartWalletValidator.sol create mode 100644 contracts/src/sigValidators/SwSignatureUtils.sol create mode 100644 contracts/test/sigValidators/SwSignatureUtils.t.sol diff --git a/contracts/src/ChannelHub.sol b/contracts/src/ChannelHub.sol index 3236d4914..cf36961da 100644 --- a/contracts/src/ChannelHub.sol +++ b/contracts/src/ChannelHub.sol @@ -831,7 +831,7 @@ contract ChannelHub is IVault, ReentrancyGuard { address user, address node, uint256 approvedSignatureValidators - ) internal view { + ) internal { (ISignatureValidator userValidator, bytes calldata userSigData) = _extractValidator(state.userSig, node, approvedSignatureValidators); _validateSignature(channelId, state, userSigData, user, userValidator); @@ -854,7 +854,7 @@ contract ChannelHub is IVault, ReentrancyGuard { bytes calldata sigData, address participant, ISignatureValidator validator - ) internal view { + ) internal { bytes memory signingData = Utils.toSigningData(state); ValidationResult result = validator.validateSignature(channelId, signingData, sigData, participant); require(ValidationResult.unwrap(result) != ValidationResult.unwrap(VALIDATION_FAILURE), IncorrectSignature()); @@ -906,7 +906,7 @@ contract ChannelHub is IVault, ReentrancyGuard { address user, address node, ParticipantIndex challengerIdx - ) internal view { + ) internal { bytes memory signingData = Utils.toSigningData(state); bytes memory challengerSigningData = abi.encodePacked(signingData, "challenge"); address challenger = challengerIdx == ParticipantIndex.USER ? user : node; diff --git a/contracts/src/interfaces/ISignatureValidator.sol b/contracts/src/interfaces/ISignatureValidator.sol index c456937fc..23e3503a7 100644 --- a/contracts/src/interfaces/ISignatureValidator.sol +++ b/contracts/src/interfaces/ISignatureValidator.sol @@ -24,6 +24,7 @@ ValidationResult constant VALIDATION_SUCCESS = ValidationResult.wrap(1); interface ISignatureValidator { /** * @notice Validates a participant's signature + * @dev Some validators (e.g., SmartWalletValidator with ERC-6492) may modify state by deploying contracts * @param channelId The channel identifier to be included in the signed message * @param signingData The encoded state data (without channelId or signatures) * @param signature The signature to validate @@ -35,5 +36,5 @@ interface ISignatureValidator { bytes calldata signingData, bytes calldata signature, address participant - ) external view returns (ValidationResult); + ) external returns (ValidationResult); } diff --git a/contracts/src/sigValidators/SmartWalletValidator.sol b/contracts/src/sigValidators/SmartWalletValidator.sol new file mode 100644 index 000000000..b78da5b77 --- /dev/null +++ b/contracts/src/sigValidators/SmartWalletValidator.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import { + ISignatureValidator, + ValidationResult, + VALIDATION_SUCCESS, + VALIDATION_FAILURE +} from "../interfaces/ISignatureValidator.sol"; +import {SwSignatureUtils} from "./SwSignatureUtils.sol"; +import {Utils} from "../Utils.sol"; + +/** + * @title SmartWalletValidator + * @notice Signature validator supporting smart contract wallets via ERC-4337 standards + * @dev Supports ERC-1271 and ERC-6492 signatures. + * + * The validator prepends channelId to the signingData to construct the full message, + * then attempts validation in the order listed above. + */ +contract SmartWalletValidator is ISignatureValidator { + /** + * @notice Validates a signature from either an EOA or smart contract wallet + * @dev Constructs the full message by prepending channelId to signingData, + * then tries validation in order: ERC-1271, ERC-6492 + * @param channelId The channel identifier to include in the signed message + * @param signingData The encoded state data (without channelId or signatures) + * @param signature The signature to validate (format varies by wallet type) + * @param participant The expected signer's address + * @return result VALIDATION_SUCCESS if valid, VALIDATION_FAILURE otherwise + */ + function validateSignature( + bytes32 channelId, + bytes calldata signingData, + bytes calldata signature, + address participant + ) external returns (ValidationResult) { + bytes memory message = Utils.pack(channelId, signingData); + + if (SwSignatureUtils.validateSmartWalletSigner(message, signature, participant)) { + return VALIDATION_SUCCESS; + } else { + return VALIDATION_FAILURE; + } + } +} diff --git a/contracts/src/sigValidators/SwSignatureUtils.sol b/contracts/src/sigValidators/SwSignatureUtils.sol new file mode 100644 index 000000000..a0786e123 --- /dev/null +++ b/contracts/src/sigValidators/SwSignatureUtils.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +/** + * @title SwSignatureUtils + * @notice Utility library for Smart Wallet signature validation + */ +library SwSignatureUtils { + using ECDSA for bytes32; + using MessageHashUtils for bytes; + + bytes4 private constant ERC1271_MAGIC_VALUE = 0x1626ba7e; + + bytes32 private constant ERC6492_MAGIC_SUFFIX = 0x6492649264926492649264926492649264926492649264926492649264926492; + + /** + * @notice Thrown when ERC-6492 contract deployment fails + * @param factory The factory address that failed to deploy + * @param factoryCalldata The calldata that was used for deployment + */ + error ERC6492DeploymentFailed(address factory, bytes factoryCalldata); + + /** + * @notice Thrown when deployed contract has no code after ERC-6492 deployment + * @param expectedSigner The address that should have been deployed + */ + error ERC6492NoCode(address expectedSigner); + + /** + * @notice Validates a signature from a smart contract wallet only (no ECDSA) + * @dev Attempts validation in order: ERC-1271 (deployed), ERC-6492 (non-deployed) + * @param message The message that was signed (will be hashed internally) + * @param signature The signature to validate (format varies by wallet type) + * @param expectedSigner The smart contract wallet address + * @return bool True if signature is valid according to ERC-1271 or ERC-6492 + */ + function validateSmartWalletSigner(bytes memory message, bytes memory signature, address expectedSigner) + internal + returns (bool) + { + bytes32 messageHash = keccak256(message); + + if (expectedSigner.code.length > 0) { + if (isValidErc1271Signature(expectedSigner, messageHash, signature)) { + return true; + } + } + + if (isERC6492Signature(signature)) { + return isValidERC6492Signature(messageHash, signature, expectedSigner); + } + + return false; + } + + /** + * @notice Validates an ERC-1271 signature for a deployed smart contract wallet + * @param signer The smart contract wallet address + * @param hash The hash of the signed message + * @param signature The signature bytes + * @return bool True if the contract returns the ERC-1271 magic value + */ + function isValidErc1271Signature(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) { + try IERC1271(signer).isValidSignature(hash, signature) returns (bytes4 magicValue) { + return magicValue == ERC1271_MAGIC_VALUE; + } catch { + return false; + } + } + + /** + * @notice Checks if a signature is wrapped in ERC-6492 format + * @dev ERC-6492 signatures end with the magic suffix + * @param signature The signature to check + * @return bool True if signature contains ERC-6492 magic suffix + */ + function isERC6492Signature(bytes memory signature) internal pure returns (bool) { + if (signature.length < 32) { + return false; + } + + bytes32 suffix; + assembly { + suffix := mload(add(signature, mload(signature))) + } + + return suffix == ERC6492_MAGIC_SUFFIX; + } + + /** + * @notice Checks the validity of a smart contract signature. If the expected signer has no code, it is deployed using the provided factory and calldata from the signature. + * Otherwise, it checks the signature using the ERC-1271 standard. + * @param msgHash The hash of the message to verify the signature against + * @param sig The signature to verify + * @param expectedSigner The address of the expected signer + * @return True if the signature is valid, false otherwise or if signer is not a contract + */ + function isValidERC6492Signature(bytes32 msgHash, bytes memory sig, address expectedSigner) + internal + returns (bool) + { + // Extract the signature data (remove the magic suffix) + uint256 dataLength = sig.length - 32; + bytes memory signatureData = new bytes(dataLength); + + for (uint256 i = 0; i < dataLength; i++) { + signatureData[i] = sig[i]; + } + + (address create2Factory, bytes memory factoryCalldata, bytes memory originalSig) = + abi.decode(signatureData, (address, bytes, bytes)); + + if (expectedSigner.code.length == 0) { + (bool success,) = create2Factory.call(factoryCalldata); + require(success, ERC6492DeploymentFailed(create2Factory, factoryCalldata)); + require(expectedSigner.code.length != 0, ERC6492NoCode(expectedSigner)); + } + + return IERC1271(expectedSigner).isValidSignature(msgHash, originalSig) == ERC1271_MAGIC_VALUE; + } +} diff --git a/contracts/test/sigValidators/SwSignatureUtils.t.sol b/contracts/test/sigValidators/SwSignatureUtils.t.sol new file mode 100644 index 000000000..ba3624744 --- /dev/null +++ b/contracts/test/sigValidators/SwSignatureUtils.t.sol @@ -0,0 +1,375 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {Test} from "forge-std/Test.sol"; + +import {TestUtils} from "../TestUtils.sol"; + +import {SwSignatureUtils} from "../../src/sigValidators/SwSignatureUtils.sol"; +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; + +/** + * @title TestSwSignatureUtils + * @notice Wrapper contract to test library functions properly + */ +contract TestSwSignatureUtils { + function validateSmartWalletSigner(bytes memory message, bytes memory signature, address expectedSigner) + external + returns (bool) + { + return SwSignatureUtils.validateSmartWalletSigner(message, signature, expectedSigner); + } +} + +/** + * @title MockSmartWallet + * @notice Mock smart contract wallet that implements ERC-1271 + */ +contract MockSmartWallet is IERC1271 { + bytes4 private constant ERC1271_MAGIC_VALUE = 0x1626ba7e; + + address public owner; + bool public shouldReturnValid; + + constructor(address _owner) { + owner = _owner; + shouldReturnValid = true; + } + + function isValidSignature(bytes32 hash, bytes memory signature) external view override returns (bytes4) { + if (!shouldReturnValid) { + return 0xffffffff; + } + + // Check signature length + if (signature.length != 65) { + return 0xffffffff; + } + + // Recover signer from signature + (uint8 v, bytes32 r, bytes32 s) = _splitSignature(signature); + address recovered = ecrecover(hash, v, r, s); + + if (recovered == owner) { + return ERC1271_MAGIC_VALUE; + } + + return 0xffffffff; + } + + function setValidation(bool _shouldReturnValid) external { + shouldReturnValid = _shouldReturnValid; + } + + function _splitSignature(bytes memory sig) private pure returns (uint8 v, bytes32 r, bytes32 s) { + assembly { + r := mload(add(sig, 32)) + s := mload(add(sig, 64)) + v := byte(0, mload(add(sig, 96))) + } + } +} + +/** + * @title MockSmartWalletFactory + * @notice Mock factory for deploying smart wallets (used for ERC-6492 testing) + */ +contract MockSmartWalletFactory { + function deploy(address owner, bytes32 salt) external payable returns (address) { + MockSmartWallet wallet = new MockSmartWallet{salt: salt}(owner); + return address(wallet); + } + + function getAddress(address owner, bytes32 salt) external view returns (address) { + bytes32 hash = keccak256( + abi.encodePacked( + bytes1(0xff), + address(this), + salt, + keccak256(abi.encodePacked(type(MockSmartWallet).creationCode, abi.encode(owner))) + ) + ); + return address(uint160(uint256(hash))); + } +} + +/** + * @title FailingFactory + * @notice Mock factory that always fails deployment + */ +contract FailingFactory { + function deploy(address, bytes32) external pure { + revert("Deployment always fails"); + } +} + +/** + * @title EmptyFactory + * @notice Mock factory that succeeds but doesn't actually deploy anything + */ +contract EmptyFactory { + function deploy(address, bytes32) external pure returns (address) { + // Succeeds but doesn't deploy anything + return address(0); + } +} + +/** + * @title SwSignatureUtilsTest_Base + * @notice Base contract for SwSignatureUtils tests with common setup and utilities + */ +contract SwSignatureUtilsTest_Base is Test { + uint256 constant OWNER_PK = 1; + uint256 constant OTHER_PK = 2; + + address owner; + address otherAccount; + + MockSmartWallet wallet; + MockSmartWalletFactory factory; + TestSwSignatureUtils swSigUtils; + + bytes constant TEST_MESSAGE = "Test message for smart wallet signature validation"; + + bytes32 private constant ERC6492_MAGIC_SUFFIX = 0x6492649264926492649264926492649264926492649264926492649264926492; + + function setUp() public virtual { + owner = vm.addr(OWNER_PK); + otherAccount = vm.addr(OTHER_PK); + + wallet = new MockSmartWallet(owner); + factory = new MockSmartWalletFactory(); + swSigUtils = new TestSwSignatureUtils(); + } + + function signMessageForWallet(uint256 privateKey, bytes memory message) internal pure returns (bytes memory) { + bytes32 messageHash = keccak256(message); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, messageHash); + return abi.encodePacked(r, s, v); + } + + function createERC6492Signature( + address factoryAddress, + bytes memory factoryCalldata, + bytes memory signature + ) internal pure returns (bytes memory) { + bytes memory wrappedData = abi.encode(factoryAddress, factoryCalldata, signature); + return abi.encodePacked(wrappedData, ERC6492_MAGIC_SUFFIX); + } + + function createFaultySignature(bytes memory validSignature) internal pure returns (bytes memory) { + require(validSignature.length == 65, "Invalid signature length"); + bytes memory faulty = new bytes(65); + for (uint256 i = 0; i < 65; i++) { + faulty[i] = validSignature[i]; + } + // Corrupt the signature by modifying the last byte of the s component + faulty[63] = bytes1(uint8(faulty[63]) ^ 0x01); + return faulty; + } +} + +/** + * @title SwSignatureUtilsTest_validateSmartWalletSigner_ERC1271 + * @notice Tests for ERC-1271 signature validation (deployed wallets) + */ +contract SwSignatureUtilsTest_validateSmartWalletSigner_ERC1271 is SwSignatureUtilsTest_Base { + function test_success_withCorrectSignature() public { + bytes memory signature = signMessageForWallet(OWNER_PK, TEST_MESSAGE); + + bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, signature, address(wallet)); + assertTrue(result, "Should validate correct ERC-1271 signature"); + } + + function test_failure_withIncorrectSigner() public { + bytes memory signature = signMessageForWallet(OTHER_PK, TEST_MESSAGE); + + bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, signature, address(wallet)); + assertFalse(result, "Should reject signature from wrong signer"); + } + + function test_failure_withFaultySignature() public { + bytes memory validSignature = signMessageForWallet(OWNER_PK, TEST_MESSAGE); + validSignature[0] = 0x42; // Corrupt the signature to make it invalid + + bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, validSignature, address(wallet)); + assertFalse(result, "Should reject faulty signature"); + } + + function test_failure_withWrongMessage() public { + bytes memory signature = signMessageForWallet(OWNER_PK, "Different message"); + + bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, signature, address(wallet)); + assertFalse(result, "Should reject signature for different message"); + } + + function test_failure_withNonERC1271Contract() public { + // Deploy a contract that doesn't implement ERC-1271 + address nonERC1271 = address(new MockSmartWalletFactory()); + bytes memory signature = signMessageForWallet(OWNER_PK, TEST_MESSAGE); + + bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, signature, nonERC1271); + assertFalse(result, "Should reject signature from non-ERC1271 contract"); + } + + function test_failure_withEOA() public { + bytes memory signature = signMessageForWallet(OWNER_PK, TEST_MESSAGE); + + bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, signature, owner); + assertFalse(result, "Should reject signature when expected signer is EOA"); + } + + function test_failure_whenWalletRejectsSignature() public { + bytes memory signature = signMessageForWallet(OWNER_PK, TEST_MESSAGE); + + // Make the wallet reject all signatures + wallet.setValidation(false); + + bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, signature, address(wallet)); + assertFalse(result, "Should reject when wallet returns invalid magic value"); + } +} + +/** + * @title SwSignatureUtilsTest_validateSmartWalletSigner_ERC6492 + * @notice Tests for ERC-6492 signature validation (non-deployed wallets) + */ +contract SwSignatureUtilsTest_validateSmartWalletSigner_ERC6492 is SwSignatureUtilsTest_Base { + function test_success_withUndeployedWallet() public { + bytes32 salt = keccak256("test_salt"); + address expectedAddress = factory.getAddress(owner, salt); + + assertEq(expectedAddress.code.length, 0, "Wallet should not be deployed yet"); + + bytes memory factoryCalldata = abi.encodeWithSelector( + MockSmartWalletFactory.deploy.selector, + owner, + salt + ); + + bytes memory signature = signMessageForWallet(OWNER_PK, TEST_MESSAGE); + bytes memory erc6492Signature = createERC6492Signature(address(factory), factoryCalldata, signature); + + bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, erc6492Signature, expectedAddress); + assertTrue(result, "Should validate ERC-6492 signature and deploy wallet"); + assertTrue(expectedAddress.code.length > 0, "Wallet should be deployed after validation"); + } + + function test_success_withAlreadyDeployedWallet() public { + bytes32 salt = keccak256("test_salt_deployed"); + // Deploy the wallet first + address deployedAddress = factory.deploy(owner, salt); + + bytes memory factoryCalldata = abi.encodeWithSelector( + MockSmartWalletFactory.deploy.selector, + owner, + salt + ); + + bytes memory signature = signMessageForWallet(OWNER_PK, TEST_MESSAGE); + bytes memory erc6492Signature = createERC6492Signature(address(factory), factoryCalldata, signature); + + bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, erc6492Signature, deployedAddress); + assertTrue(result, "Should validate ERC-6492 signature for already deployed wallet"); + } + + function test_failure_withInvalidERC6492Signature_nonDeployed() public { + bytes32 salt = keccak256("test_salt_invalid"); + address expectedAddress = factory.getAddress(owner, salt); + + bytes memory factoryCalldata = abi.encodeWithSelector( + MockSmartWalletFactory.deploy.selector, + owner, + salt + ); + + // Use wrong private key + bytes memory signature = signMessageForWallet(OTHER_PK, TEST_MESSAGE); + bytes memory erc6492Signature = createERC6492Signature(address(factory), factoryCalldata, signature); + + // Should deploy the contract but reject the invalid signature + bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, erc6492Signature, expectedAddress); + assertFalse(result, "Should reject invalid ERC-6492 signature"); + assertTrue(expectedAddress.code.length > 0, "Wallet should be deployed even if signature is invalid"); + } + + function test_failure_withInvalidERC6492Signature_deployed() public { + bytes32 salt = keccak256("test_salt_wrong_sig"); + // Deploy the wallet first + address deployedAddress = factory.deploy(owner, salt); + + bytes memory factoryCalldata = abi.encodeWithSelector( + MockSmartWalletFactory.deploy.selector, + owner, + salt + ); + + // Sign with wrong private key + bytes memory signature = signMessageForWallet(OTHER_PK, TEST_MESSAGE); + bytes memory erc6492Signature = createERC6492Signature(address(factory), factoryCalldata, signature); + + // Should fail because signature is from wrong signer + bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, erc6492Signature, deployedAddress); + assertFalse(result, "Should reject ERC-6492 signature with wrong signer for deployed wallet"); + } + + function test_failure_withShortSignature() public { + // Create a signature shorter than 32 bytes (won't be detected as ERC-6492) + bytes memory shortSig = new bytes(16); + + bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, shortSig, address(wallet)); + assertFalse(result, "Should reject signature shorter than 32 bytes"); + } + + function test_revert_ERC6492DeploymentFailed() public { + FailingFactory failFactory = new FailingFactory(); + bytes32 salt = keccak256("test_salt_deployment_failed"); + address expectedAddress = address(1); // The address that would be deployed (not important since deployment fails) + + // Create calldata for failing factory + bytes memory failingCalldata = abi.encodeWithSelector( + FailingFactory.deploy.selector, + owner, + salt + ); + + bytes memory signature = signMessageForWallet(OWNER_PK, TEST_MESSAGE); + bytes memory erc6492Signature = createERC6492Signature(address(failFactory), failingCalldata, signature); + + vm.expectRevert( + abi.encodeWithSelector( + SwSignatureUtils.ERC6492DeploymentFailed.selector, + address(failFactory), + failingCalldata + ) + ); + swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, erc6492Signature, expectedAddress); + } + + function test_revert_ERC6492NoCode() public { + // Deploy a special factory that doesn't revert but also doesn't deploy to expected address + EmptyFactory emptyFactory = new EmptyFactory(); + bytes32 salt = keccak256("test_salt_no_code"); + + // Create a counterfactual address that won't have code + address expectedAddress = address(0x1234567890123456789012345678901234567890); + + bytes memory factoryCalldata = abi.encodeWithSelector( + EmptyFactory.deploy.selector, + owner, + salt + ); + + bytes memory signature = signMessageForWallet(OWNER_PK, TEST_MESSAGE); + bytes memory erc6492Signature = createERC6492Signature(address(emptyFactory), factoryCalldata, signature); + + // Should revert with ERC6492NoCode because the expected address doesn't have code after deployment + vm.expectRevert( + abi.encodeWithSelector( + SwSignatureUtils.ERC6492NoCode.selector, + expectedAddress + ) + ); + swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, erc6492Signature, expectedAddress); + } +} From d4939a4a6225c994b64aca18672ba59a0bdec83a Mon Sep 17 00:00:00 2001 From: nksazonov Date: Mon, 23 Feb 2026 13:51:34 +0100 Subject: [PATCH 4/6] test(contracts): refactor, add SmartWalletValidator tests --- .../sigValidators/SessionKeyValidator.t.sol | 24 +-- .../sigValidators/SmartWalletValidator.t.sol | 188 ++++++++++++++++++ .../test/sigValidators/SwSignatureUtils.t.sol | 161 ++------------- contracts/test/sigValidators/SwTestUtils.sol | 110 ++++++++++ 4 files changed, 330 insertions(+), 153 deletions(-) create mode 100644 contracts/test/sigValidators/SmartWalletValidator.t.sol create mode 100644 contracts/test/sigValidators/SwTestUtils.sol diff --git a/contracts/test/sigValidators/SessionKeyValidator.t.sol b/contracts/test/sigValidators/SessionKeyValidator.t.sol index df0ae013c..798f259f2 100644 --- a/contracts/test/sigValidators/SessionKeyValidator.t.sol +++ b/contracts/test/sigValidators/SessionKeyValidator.t.sol @@ -95,7 +95,7 @@ contract SessionKeyValidatorTest_Base is Test { } contract SessionKeyValidatorTest_validateSignature is SessionKeyValidatorTest_Base { - function test_success_withBothEip191() public view { + function test_success_withBothEip191() public { SessionKeyAuthorization memory skAuth = createSkAuth(sessionKey1, METADATA_HASH, USER_PK, true); bytes memory skSignature = signStateWithSk(CHANNEL_ID, SIGNING_DATA, SESSION_KEY1_PK, true); bytes memory signature = abi.encode(skAuth, skSignature); @@ -104,7 +104,7 @@ contract SessionKeyValidatorTest_validateSignature is SessionKeyValidatorTest_Ba assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_SUCCESS)); } - function test_success_withBothRaw() public view { + function test_success_withBothRaw() public { SessionKeyAuthorization memory skAuth = createSkAuth(sessionKey1, METADATA_HASH, USER_PK, false); bytes memory skSignature = signStateWithSk(CHANNEL_ID, SIGNING_DATA, SESSION_KEY1_PK, false); bytes memory signature = abi.encode(skAuth, skSignature); @@ -113,7 +113,7 @@ contract SessionKeyValidatorTest_validateSignature is SessionKeyValidatorTest_Ba assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_SUCCESS)); } - function test_success_withAuthEip191SkSigRaw() public view { + function test_success_withAuthEip191SkSigRaw() public { SessionKeyAuthorization memory skAuth = createSkAuth(sessionKey1, METADATA_HASH, USER_PK, true); bytes memory skSignature = signStateWithSk(CHANNEL_ID, SIGNING_DATA, SESSION_KEY1_PK, false); bytes memory signature = abi.encode(skAuth, skSignature); @@ -122,7 +122,7 @@ contract SessionKeyValidatorTest_validateSignature is SessionKeyValidatorTest_Ba assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_SUCCESS)); } - function test_success_withAuthRawSkSigEip191() public view { + function test_success_withAuthRawSkSigEip191() public { SessionKeyAuthorization memory skAuth = createSkAuth(sessionKey1, METADATA_HASH, USER_PK, false); bytes memory skSignature = signStateWithSk(CHANNEL_ID, SIGNING_DATA, SESSION_KEY1_PK, true); bytes memory signature = abi.encode(skAuth, skSignature); @@ -131,7 +131,7 @@ contract SessionKeyValidatorTest_validateSignature is SessionKeyValidatorTest_Ba assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_SUCCESS)); } - function test_failure_withSkAuthNotSignedByParticipant_eip191() public view { + function test_failure_withSkAuthNotSignedByParticipant_eip191() public { SessionKeyAuthorization memory skAuth = createSkAuth(sessionKey1, METADATA_HASH, OTHER_SIGNER_PK, true); bytes memory skSignature = signStateWithSk(CHANNEL_ID, SIGNING_DATA, SESSION_KEY1_PK, true); bytes memory signature = abi.encode(skAuth, skSignature); @@ -140,7 +140,7 @@ contract SessionKeyValidatorTest_validateSignature is SessionKeyValidatorTest_Ba assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); } - function test_failure_withSkAuthNotSignedByParticipant_raw() public view { + function test_failure_withSkAuthNotSignedByParticipant_raw() public { SessionKeyAuthorization memory skAuth = createSkAuth(sessionKey1, METADATA_HASH, OTHER_SIGNER_PK, false); bytes memory skSignature = signStateWithSk(CHANNEL_ID, SIGNING_DATA, SESSION_KEY1_PK, false); bytes memory signature = abi.encode(skAuth, skSignature); @@ -149,7 +149,7 @@ contract SessionKeyValidatorTest_validateSignature is SessionKeyValidatorTest_Ba assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); } - function test_failure_withSigningDataNotSignedBySessionKey_eip191() public view { + function test_failure_withSigningDataNotSignedBySessionKey_eip191() public { SessionKeyAuthorization memory skAuth = createSkAuth(sessionKey1, METADATA_HASH, USER_PK, true); bytes memory skSignature = signStateWithSk(CHANNEL_ID, SIGNING_DATA, SESSION_KEY2_PK, true); bytes memory signature = abi.encode(skAuth, skSignature); @@ -158,7 +158,7 @@ contract SessionKeyValidatorTest_validateSignature is SessionKeyValidatorTest_Ba assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); } - function test_failure_withSigningDataNotSignedBySessionKey_raw() public view { + function test_failure_withSigningDataNotSignedBySessionKey_raw() public { SessionKeyAuthorization memory skAuth = createSkAuth(sessionKey1, METADATA_HASH, USER_PK, false); bytes memory skSignature = signStateWithSk(CHANNEL_ID, SIGNING_DATA, SESSION_KEY2_PK, false); bytes memory signature = abi.encode(skAuth, skSignature); @@ -167,7 +167,7 @@ contract SessionKeyValidatorTest_validateSignature is SessionKeyValidatorTest_Ba assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); } - function test_failure_withOtherMetadataHash_eip191() public view { + function test_failure_withOtherMetadataHash_eip191() public { SessionKeyAuthorization memory skAuth = createSkAuth(sessionKey1, OTHER_METADATA_HASH, USER_PK, true); bytes memory skSignature = signStateWithSk(CHANNEL_ID, SIGNING_DATA, SESSION_KEY1_PK, true); bytes memory signature = abi.encode(skAuth, skSignature); @@ -179,7 +179,7 @@ contract SessionKeyValidatorTest_validateSignature is SessionKeyValidatorTest_Ba assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); } - function test_failure_withOtherMetadataHash_raw() public view { + function test_failure_withOtherMetadataHash_raw() public { SessionKeyAuthorization memory skAuth = createSkAuth(sessionKey1, OTHER_METADATA_HASH, USER_PK, false); bytes memory skSignature = signStateWithSk(CHANNEL_ID, SIGNING_DATA, SESSION_KEY1_PK, false); bytes memory signature = abi.encode(skAuth, skSignature); @@ -191,7 +191,7 @@ contract SessionKeyValidatorTest_validateSignature is SessionKeyValidatorTest_Ba assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); } - function test_failure_withOtherSigningData_eip191() public view { + function test_failure_withOtherSigningData_eip191() public { SessionKeyAuthorization memory skAuth = createSkAuth(sessionKey1, METADATA_HASH, USER_PK, true); bytes memory skSignature = signStateWithSk(CHANNEL_ID, OTHER_SIGNING_DATA, SESSION_KEY1_PK, true); bytes memory signature = abi.encode(skAuth, skSignature); @@ -200,7 +200,7 @@ contract SessionKeyValidatorTest_validateSignature is SessionKeyValidatorTest_Ba assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); } - function test_failure_withOtherSigningData_raw() public view { + function test_failure_withOtherSigningData_raw() public { SessionKeyAuthorization memory skAuth = createSkAuth(sessionKey1, METADATA_HASH, USER_PK, false); bytes memory skSignature = signStateWithSk(CHANNEL_ID, OTHER_SIGNING_DATA, SESSION_KEY1_PK, false); bytes memory signature = abi.encode(skAuth, skSignature); diff --git a/contracts/test/sigValidators/SmartWalletValidator.t.sol b/contracts/test/sigValidators/SmartWalletValidator.t.sol new file mode 100644 index 000000000..410da81a4 --- /dev/null +++ b/contracts/test/sigValidators/SmartWalletValidator.t.sol @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {Test} from "forge-std/Test.sol"; + +import {TestUtils} from "../TestUtils.sol"; +import {MockSmartWallet, MockSmartWalletFactory, SwTestUtils} from "./SwTestUtils.sol"; + +import {SmartWalletValidator} from "../../src/sigValidators/SmartWalletValidator.sol"; +import {ValidationResult, VALIDATION_SUCCESS, VALIDATION_FAILURE} from "../../src/interfaces/ISignatureValidator.sol"; +import {Utils} from "../../src/Utils.sol"; + +contract SmartWalletValidatorTest_Base is Test { + SmartWalletValidator public validator; + + uint256 constant USER_PK = 1; + uint256 constant OTHER_SIGNER_PK = 2; + + address user; + address otherSigner; + + MockSmartWallet userSw; + MockSmartWallet otherSw; + MockSmartWalletFactory factory; + + bytes32 constant CHANNEL_ID = keccak256("test-channel"); + bytes32 constant OTHER_CHANNEL_ID = keccak256("other-channel"); + bytes constant SIGNING_DATA = hex"1234567890abcdef"; + + function setUp() public virtual { + validator = new SmartWalletValidator(); + + user = vm.addr(USER_PK); + otherSigner = vm.addr(OTHER_SIGNER_PK); + + userSw = new MockSmartWallet(user); + otherSw = new MockSmartWallet(otherSigner); + factory = new MockSmartWalletFactory(); + } +} + +/** + * @title SmartWalletValidatorTest_validateSignature_ERC1271 + * @notice Tests for ERC-1271 signature validation (deployed wallets) + */ +contract SmartWalletValidatorTest_validateSignature_ERC1271 is SmartWalletValidatorTest_Base { + function test_success_withCorrectSignature() public { + bytes memory message = Utils.pack(CHANNEL_ID, SIGNING_DATA); + bytes memory signature = TestUtils.signRaw(vm, USER_PK, message); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, signature, address(userSw)); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_SUCCESS)); + } + + function test_failure_withIncorrectSigner() public { + bytes memory message = Utils.pack(CHANNEL_ID, SIGNING_DATA); + // use other signer + bytes memory signature = TestUtils.signRaw(vm, OTHER_SIGNER_PK, message); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, signature, address(userSw)); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); + } + + function test_failure_withWrongChannelId() public { + // use wrong channel Id + bytes memory message = Utils.pack(OTHER_CHANNEL_ID, SIGNING_DATA); + bytes memory signature = TestUtils.signRaw(vm, USER_PK, message); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, signature, address(userSw)); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); + } + + function test_failure_withFaultySignature() public { + bytes memory message = Utils.pack(CHANNEL_ID, SIGNING_DATA); + bytes memory validSignature = TestUtils.signRaw(vm, USER_PK, message); + validSignature[0] = 0x42; // Corrupt the signature + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, validSignature, address(userSw)); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); + } + + function test_failure_withEOA() public { + bytes memory message = Utils.pack(CHANNEL_ID, SIGNING_DATA); + bytes memory signature = TestUtils.signRaw(vm, USER_PK, message); + + // Try to validate against EOA address instead of wallet + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, signature, user); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); + } + + function test_failure_whenWalletRejectsSignature() public { + bytes memory message = Utils.pack(CHANNEL_ID, SIGNING_DATA); + bytes memory signature = TestUtils.signRaw(vm, USER_PK, message); + + // Make the wallet reject all signatures + userSw.setValidation(false); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, signature, address(userSw)); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); + } +} + +/** + * @title SmartWalletValidatorTest_validateSignature_ERC6492 + * @notice Tests for ERC-6492 signature validation (non-deployed wallets) + */ +contract SmartWalletValidatorTest_validateSignature_ERC6492 is SmartWalletValidatorTest_Base { + function test_success_withNonDeployedWallet() public { + bytes32 salt = keccak256("test_salt"); + address expectedAddress = factory.getAddress(user, salt); + + // Ensure wallet is not deployed + assertEq(expectedAddress.code.length, 0, "Wallet should not be deployed yet"); + + bytes memory factoryCalldata = abi.encodeWithSelector( + MockSmartWalletFactory.deploy.selector, + user, + salt + ); + + bytes memory message = Utils.pack(CHANNEL_ID, SIGNING_DATA); + bytes memory signature = TestUtils.signRaw(vm, USER_PK, message); + bytes memory erc6492Signature = SwTestUtils.createERC6492Signature(address(factory), factoryCalldata, signature); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, erc6492Signature, expectedAddress); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_SUCCESS)); + assertTrue(expectedAddress.code.length > 0, "Wallet should be deployed after validation"); + } + + function test_success_withAlreadyDeployedWallet() public { + bytes32 salt = keccak256("test_salt_deployed"); + + // Deploy the wallet first + address deployedAddress = factory.deploy(user, salt); + + bytes memory factoryCalldata = abi.encodeWithSelector( + MockSmartWalletFactory.deploy.selector, + user, + salt + ); + + bytes memory message = Utils.pack(CHANNEL_ID, SIGNING_DATA); + bytes memory signature = TestUtils.signRaw(vm, USER_PK, message); + bytes memory erc6492Signature = SwTestUtils.createERC6492Signature(address(factory), factoryCalldata, signature); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, erc6492Signature, deployedAddress); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_SUCCESS)); + } + + function test_failure_withInvalidSignature() public { + bytes32 salt = keccak256("test_salt_invalid"); + address expectedAddress = factory.getAddress(user, salt); + + bytes memory factoryCalldata = abi.encodeWithSelector( + MockSmartWalletFactory.deploy.selector, + user, + salt + ); + + // Use wrong private key + bytes memory message = Utils.pack(CHANNEL_ID, SIGNING_DATA); + bytes memory signature = TestUtils.signRaw(vm, OTHER_SIGNER_PK, message); + bytes memory erc6492Signature = SwTestUtils.createERC6492Signature(address(factory), factoryCalldata, signature); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, erc6492Signature, expectedAddress); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); + assertTrue(expectedAddress.code.length > 0, "Wallet should be deployed even if signature is invalid"); + } + + function test_failure_withWrongChannelId() public { + bytes32 salt = keccak256("test_salt_wrong_channel"); + address expectedAddress = factory.getAddress(user, salt); + + bytes memory factoryCalldata = abi.encodeWithSelector( + MockSmartWalletFactory.deploy.selector, + user, + salt + ); + + // Sign with wrong channel ID + bytes memory message = Utils.pack(OTHER_CHANNEL_ID, SIGNING_DATA); + bytes memory signature = TestUtils.signRaw(vm, USER_PK, message); + bytes memory erc6492Signature = SwTestUtils.createERC6492Signature(address(factory), factoryCalldata, signature); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, erc6492Signature, expectedAddress); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); + } +} diff --git a/contracts/test/sigValidators/SwSignatureUtils.t.sol b/contracts/test/sigValidators/SwSignatureUtils.t.sol index ba3624744..5eb7af17b 100644 --- a/contracts/test/sigValidators/SwSignatureUtils.t.sol +++ b/contracts/test/sigValidators/SwSignatureUtils.t.sol @@ -4,9 +4,9 @@ pragma solidity 0.8.30; import {Test} from "forge-std/Test.sol"; import {TestUtils} from "../TestUtils.sol"; +import {MockSmartWallet, MockSmartWalletFactory, FailingFactory, EmptyFactory, SwTestUtils} from "./SwTestUtils.sol"; import {SwSignatureUtils} from "../../src/sigValidators/SwSignatureUtils.sol"; -import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; /** * @title TestSwSignatureUtils @@ -21,99 +21,6 @@ contract TestSwSignatureUtils { } } -/** - * @title MockSmartWallet - * @notice Mock smart contract wallet that implements ERC-1271 - */ -contract MockSmartWallet is IERC1271 { - bytes4 private constant ERC1271_MAGIC_VALUE = 0x1626ba7e; - - address public owner; - bool public shouldReturnValid; - - constructor(address _owner) { - owner = _owner; - shouldReturnValid = true; - } - - function isValidSignature(bytes32 hash, bytes memory signature) external view override returns (bytes4) { - if (!shouldReturnValid) { - return 0xffffffff; - } - - // Check signature length - if (signature.length != 65) { - return 0xffffffff; - } - - // Recover signer from signature - (uint8 v, bytes32 r, bytes32 s) = _splitSignature(signature); - address recovered = ecrecover(hash, v, r, s); - - if (recovered == owner) { - return ERC1271_MAGIC_VALUE; - } - - return 0xffffffff; - } - - function setValidation(bool _shouldReturnValid) external { - shouldReturnValid = _shouldReturnValid; - } - - function _splitSignature(bytes memory sig) private pure returns (uint8 v, bytes32 r, bytes32 s) { - assembly { - r := mload(add(sig, 32)) - s := mload(add(sig, 64)) - v := byte(0, mload(add(sig, 96))) - } - } -} - -/** - * @title MockSmartWalletFactory - * @notice Mock factory for deploying smart wallets (used for ERC-6492 testing) - */ -contract MockSmartWalletFactory { - function deploy(address owner, bytes32 salt) external payable returns (address) { - MockSmartWallet wallet = new MockSmartWallet{salt: salt}(owner); - return address(wallet); - } - - function getAddress(address owner, bytes32 salt) external view returns (address) { - bytes32 hash = keccak256( - abi.encodePacked( - bytes1(0xff), - address(this), - salt, - keccak256(abi.encodePacked(type(MockSmartWallet).creationCode, abi.encode(owner))) - ) - ); - return address(uint160(uint256(hash))); - } -} - -/** - * @title FailingFactory - * @notice Mock factory that always fails deployment - */ -contract FailingFactory { - function deploy(address, bytes32) external pure { - revert("Deployment always fails"); - } -} - -/** - * @title EmptyFactory - * @notice Mock factory that succeeds but doesn't actually deploy anything - */ -contract EmptyFactory { - function deploy(address, bytes32) external pure returns (address) { - // Succeeds but doesn't deploy anything - return address(0); - } -} - /** * @title SwSignatureUtilsTest_Base * @notice Base contract for SwSignatureUtils tests with common setup and utilities @@ -131,8 +38,6 @@ contract SwSignatureUtilsTest_Base is Test { bytes constant TEST_MESSAGE = "Test message for smart wallet signature validation"; - bytes32 private constant ERC6492_MAGIC_SUFFIX = 0x6492649264926492649264926492649264926492649264926492649264926492; - function setUp() public virtual { owner = vm.addr(OWNER_PK); otherAccount = vm.addr(OTHER_PK); @@ -141,32 +46,6 @@ contract SwSignatureUtilsTest_Base is Test { factory = new MockSmartWalletFactory(); swSigUtils = new TestSwSignatureUtils(); } - - function signMessageForWallet(uint256 privateKey, bytes memory message) internal pure returns (bytes memory) { - bytes32 messageHash = keccak256(message); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, messageHash); - return abi.encodePacked(r, s, v); - } - - function createERC6492Signature( - address factoryAddress, - bytes memory factoryCalldata, - bytes memory signature - ) internal pure returns (bytes memory) { - bytes memory wrappedData = abi.encode(factoryAddress, factoryCalldata, signature); - return abi.encodePacked(wrappedData, ERC6492_MAGIC_SUFFIX); - } - - function createFaultySignature(bytes memory validSignature) internal pure returns (bytes memory) { - require(validSignature.length == 65, "Invalid signature length"); - bytes memory faulty = new bytes(65); - for (uint256 i = 0; i < 65; i++) { - faulty[i] = validSignature[i]; - } - // Corrupt the signature by modifying the last byte of the s component - faulty[63] = bytes1(uint8(faulty[63]) ^ 0x01); - return faulty; - } } /** @@ -175,21 +54,21 @@ contract SwSignatureUtilsTest_Base is Test { */ contract SwSignatureUtilsTest_validateSmartWalletSigner_ERC1271 is SwSignatureUtilsTest_Base { function test_success_withCorrectSignature() public { - bytes memory signature = signMessageForWallet(OWNER_PK, TEST_MESSAGE); + bytes memory signature = TestUtils.signRaw(vm, OWNER_PK, TEST_MESSAGE); bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, signature, address(wallet)); assertTrue(result, "Should validate correct ERC-1271 signature"); } function test_failure_withIncorrectSigner() public { - bytes memory signature = signMessageForWallet(OTHER_PK, TEST_MESSAGE); + bytes memory signature = TestUtils.signRaw(vm, OTHER_PK, TEST_MESSAGE); bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, signature, address(wallet)); assertFalse(result, "Should reject signature from wrong signer"); } function test_failure_withFaultySignature() public { - bytes memory validSignature = signMessageForWallet(OWNER_PK, TEST_MESSAGE); + bytes memory validSignature = TestUtils.signRaw(vm, OWNER_PK, TEST_MESSAGE); validSignature[0] = 0x42; // Corrupt the signature to make it invalid bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, validSignature, address(wallet)); @@ -197,7 +76,7 @@ contract SwSignatureUtilsTest_validateSmartWalletSigner_ERC1271 is SwSignatureUt } function test_failure_withWrongMessage() public { - bytes memory signature = signMessageForWallet(OWNER_PK, "Different message"); + bytes memory signature = TestUtils.signRaw(vm, OWNER_PK, "Different message"); bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, signature, address(wallet)); assertFalse(result, "Should reject signature for different message"); @@ -206,21 +85,21 @@ contract SwSignatureUtilsTest_validateSmartWalletSigner_ERC1271 is SwSignatureUt function test_failure_withNonERC1271Contract() public { // Deploy a contract that doesn't implement ERC-1271 address nonERC1271 = address(new MockSmartWalletFactory()); - bytes memory signature = signMessageForWallet(OWNER_PK, TEST_MESSAGE); + bytes memory signature = TestUtils.signRaw(vm, OWNER_PK, TEST_MESSAGE); bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, signature, nonERC1271); assertFalse(result, "Should reject signature from non-ERC1271 contract"); } function test_failure_withEOA() public { - bytes memory signature = signMessageForWallet(OWNER_PK, TEST_MESSAGE); + bytes memory signature = TestUtils.signRaw(vm, OWNER_PK, TEST_MESSAGE); bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, signature, owner); assertFalse(result, "Should reject signature when expected signer is EOA"); } function test_failure_whenWalletRejectsSignature() public { - bytes memory signature = signMessageForWallet(OWNER_PK, TEST_MESSAGE); + bytes memory signature = TestUtils.signRaw(vm, OWNER_PK, TEST_MESSAGE); // Make the wallet reject all signatures wallet.setValidation(false); @@ -247,8 +126,8 @@ contract SwSignatureUtilsTest_validateSmartWalletSigner_ERC6492 is SwSignatureUt salt ); - bytes memory signature = signMessageForWallet(OWNER_PK, TEST_MESSAGE); - bytes memory erc6492Signature = createERC6492Signature(address(factory), factoryCalldata, signature); + bytes memory signature = TestUtils.signRaw(vm, OWNER_PK, TEST_MESSAGE); + bytes memory erc6492Signature = SwTestUtils.createERC6492Signature(address(factory), factoryCalldata, signature); bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, erc6492Signature, expectedAddress); assertTrue(result, "Should validate ERC-6492 signature and deploy wallet"); @@ -266,8 +145,8 @@ contract SwSignatureUtilsTest_validateSmartWalletSigner_ERC6492 is SwSignatureUt salt ); - bytes memory signature = signMessageForWallet(OWNER_PK, TEST_MESSAGE); - bytes memory erc6492Signature = createERC6492Signature(address(factory), factoryCalldata, signature); + bytes memory signature = TestUtils.signRaw(vm, OWNER_PK, TEST_MESSAGE); + bytes memory erc6492Signature = SwTestUtils.createERC6492Signature(address(factory), factoryCalldata, signature); bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, erc6492Signature, deployedAddress); assertTrue(result, "Should validate ERC-6492 signature for already deployed wallet"); @@ -284,8 +163,8 @@ contract SwSignatureUtilsTest_validateSmartWalletSigner_ERC6492 is SwSignatureUt ); // Use wrong private key - bytes memory signature = signMessageForWallet(OTHER_PK, TEST_MESSAGE); - bytes memory erc6492Signature = createERC6492Signature(address(factory), factoryCalldata, signature); + bytes memory signature = TestUtils.signRaw(vm, OTHER_PK, TEST_MESSAGE); + bytes memory erc6492Signature = SwTestUtils.createERC6492Signature(address(factory), factoryCalldata, signature); // Should deploy the contract but reject the invalid signature bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, erc6492Signature, expectedAddress); @@ -305,8 +184,8 @@ contract SwSignatureUtilsTest_validateSmartWalletSigner_ERC6492 is SwSignatureUt ); // Sign with wrong private key - bytes memory signature = signMessageForWallet(OTHER_PK, TEST_MESSAGE); - bytes memory erc6492Signature = createERC6492Signature(address(factory), factoryCalldata, signature); + bytes memory signature = TestUtils.signRaw(vm, OTHER_PK, TEST_MESSAGE); + bytes memory erc6492Signature = SwTestUtils.createERC6492Signature(address(factory), factoryCalldata, signature); // Should fail because signature is from wrong signer bool result = swSigUtils.validateSmartWalletSigner(TEST_MESSAGE, erc6492Signature, deployedAddress); @@ -333,8 +212,8 @@ contract SwSignatureUtilsTest_validateSmartWalletSigner_ERC6492 is SwSignatureUt salt ); - bytes memory signature = signMessageForWallet(OWNER_PK, TEST_MESSAGE); - bytes memory erc6492Signature = createERC6492Signature(address(failFactory), failingCalldata, signature); + bytes memory signature = TestUtils.signRaw(vm, OWNER_PK, TEST_MESSAGE); + bytes memory erc6492Signature = SwTestUtils.createERC6492Signature(address(failFactory), failingCalldata, signature); vm.expectRevert( abi.encodeWithSelector( @@ -360,8 +239,8 @@ contract SwSignatureUtilsTest_validateSmartWalletSigner_ERC6492 is SwSignatureUt salt ); - bytes memory signature = signMessageForWallet(OWNER_PK, TEST_MESSAGE); - bytes memory erc6492Signature = createERC6492Signature(address(emptyFactory), factoryCalldata, signature); + bytes memory signature = TestUtils.signRaw(vm, OWNER_PK, TEST_MESSAGE); + bytes memory erc6492Signature = SwTestUtils.createERC6492Signature(address(emptyFactory), factoryCalldata, signature); // Should revert with ERC6492NoCode because the expected address doesn't have code after deployment vm.expectRevert( diff --git a/contracts/test/sigValidators/SwTestUtils.sol b/contracts/test/sigValidators/SwTestUtils.sol new file mode 100644 index 000000000..9a2b207da --- /dev/null +++ b/contracts/test/sigValidators/SwTestUtils.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; + +/** + * @title MockSmartWallet + * @notice Mock smart contract wallet that implements ERC-1271 + */ +contract MockSmartWallet is IERC1271 { + bytes4 private constant ERC1271_MAGIC_VALUE = 0x1626ba7e; + + address public owner; + bool public shouldReturnValid; + + constructor(address _owner) { + owner = _owner; + shouldReturnValid = true; + } + + function isValidSignature(bytes32 hash, bytes memory signature) external view override returns (bytes4) { + if (!shouldReturnValid) { + return 0xffffffff; + } + + // Check signature length + if (signature.length != 65) { + return 0xffffffff; + } + + // Recover signer from signature + (uint8 v, bytes32 r, bytes32 s) = _splitSignature(signature); + address recovered = ecrecover(hash, v, r, s); + + if (recovered == owner) { + return ERC1271_MAGIC_VALUE; + } + + return 0xffffffff; + } + + function setValidation(bool _shouldReturnValid) external { + shouldReturnValid = _shouldReturnValid; + } + + function _splitSignature(bytes memory sig) private pure returns (uint8 v, bytes32 r, bytes32 s) { + assembly { + r := mload(add(sig, 32)) + s := mload(add(sig, 64)) + v := byte(0, mload(add(sig, 96))) + } + } +} + +/** + * @title MockSmartWalletFactory + * @notice Mock factory for deploying smart wallets (used for ERC-6492 testing) + */ +contract MockSmartWalletFactory { + function deploy(address owner, bytes32 salt) external payable returns (address) { + MockSmartWallet wallet = new MockSmartWallet{salt: salt}(owner); + return address(wallet); + } + + function getAddress(address owner, bytes32 salt) external view returns (address) { + bytes32 hash = keccak256( + abi.encodePacked( + bytes1(0xff), + address(this), + salt, + keccak256(abi.encodePacked(type(MockSmartWallet).creationCode, abi.encode(owner))) + ) + ); + return address(uint160(uint256(hash))); + } +} + +/** + * @title FailingFactory + * @notice Mock factory that always fails deployment + */ +contract FailingFactory { + function deploy(address, bytes32) external pure { + revert("Deployment always fails"); + } +} + +/** + * @title EmptyFactory + * @notice Mock factory that succeeds but doesn't actually deploy anything + */ +contract EmptyFactory { + function deploy(address, bytes32) external pure returns (address) { + // Succeeds but doesn't deploy anything + return address(0); + } +} + +bytes32 constant ERC6492_MAGIC_SUFFIX = 0x6492649264926492649264926492649264926492649264926492649264926492; + +library SwTestUtils { + function createERC6492Signature( + address factoryAddress, + bytes memory factoryCalldata, + bytes memory signature + ) internal pure returns (bytes memory) { + bytes memory wrappedData = abi.encode(factoryAddress, factoryCalldata, signature); + return abi.encodePacked(wrappedData, ERC6492_MAGIC_SUFFIX); + } +} From 1e0c02c41c72a13d7ef53fad4e85b0f041191684 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Mon, 23 Feb 2026 14:15:16 +0100 Subject: [PATCH 5/6] feat(contracts): add smart wallet support for SessionKeyValidator --- .../src/sigValidators/EcdsaSignatureUtils.sol | 7 + .../src/sigValidators/SessionKeyValidator.sol | 37 +- .../sigValidators/SessionKeyValidator.t.sol | 347 ++++++++++++++++++ 3 files changed, 386 insertions(+), 5 deletions(-) diff --git a/contracts/src/sigValidators/EcdsaSignatureUtils.sol b/contracts/src/sigValidators/EcdsaSignatureUtils.sol index 05d079e8f..37cc8f54b 100644 --- a/contracts/src/sigValidators/EcdsaSignatureUtils.sol +++ b/contracts/src/sigValidators/EcdsaSignatureUtils.sol @@ -18,6 +18,7 @@ library EcdsaSignatureUtils { * @notice Validates that a signature was created by an expected signer * @dev Tries EIP-191 recovery first (with Ethereum signed message prefix), then raw ECDSA if that fails. * Hashes the message internally before recovery. + * Returns false for invalid signature formats (e.g., wrong length). * @param message The message that was signed (will be hashed internally) * @param signature The signature to validate (65 bytes ECDSA: r, s, v) * @param expectedSigner The address that should have signed the message @@ -28,6 +29,12 @@ library EcdsaSignatureUtils { pure returns (bool) { + // ECDSA signatures must be exactly 65 bytes (r: 32, s: 32, v: 1) + // Return false for other lengths (e.g., ERC-6492 wrapped signatures) + if (signature.length != 65) { + return false; + } + bytes32 eip191Digest = message.toEthSignedMessageHash(); address recovered = eip191Digest.recover(signature); diff --git a/contracts/src/sigValidators/SessionKeyValidator.sol b/contracts/src/sigValidators/SessionKeyValidator.sol index 34c8288a3..4b6a9373b 100644 --- a/contracts/src/sigValidators/SessionKeyValidator.sol +++ b/contracts/src/sigValidators/SessionKeyValidator.sol @@ -8,6 +8,7 @@ import { VALIDATION_FAILURE } from "../interfaces/ISignatureValidator.sol"; import {EcdsaSignatureUtils} from "./EcdsaSignatureUtils.sol"; +import {SwSignatureUtils} from "./SwSignatureUtils.sol"; import {Utils} from "../Utils.sol"; /** @@ -36,6 +37,7 @@ function toSigningData(SessionKeyAuthorization memory skAuth) pure returns (byte * @notice Validator supporting session key delegation for temporary signing authority * @dev Enables a participant to delegate signing authority to a session key with metadata. * Useful for hot wallets, time-limited access, or gasless transactions. + * Supports both EOA (ECDSA) and smart contract wallet (ERC-1271/ERC-6492) signatures. * * Authorization Flow: * 1. Participant signs a SessionKeyAuthorization to delegate to a session key @@ -45,7 +47,10 @@ function toSigningData(SessionKeyAuthorization memory skAuth) pure returns (byte * Signature Format: * bytes sigBody = abi.encode(SessionKeyAuthorization skAuthorization, bytes signature) * - * Where signature is a standard 65-byte EIP-191 or raw ECDSA signature of the packed state. + * Where signature can be: + * - Standard 65-byte EIP-191 or raw ECDSA signature (for EOAs) + * - ERC-1271 signature (for deployed smart wallets) + * - ERC-6492 signature (for undeployed smart wallets) * * Security Model: * - Off-chain enforcement (Clearnode) should validate session key expiration and usage limits @@ -58,7 +63,8 @@ contract SessionKeyValidator is ISignatureValidator { * @dev Validates: * 1. participant signed the SessionKeyAuthorization (with channelId binding) * 2. sessionKey signed the full state message (channelId + signingData) - * Tries EIP-191 recovery first, then raw ECDSA for both signatures. + * Supports both EOA (ECDSA) and smart wallet (ERC-1271/ERC-6492) signatures. + * Tries ECDSA validation first, then smart wallet validation for both signatures. * @param channelId The channel identifier to include in state messages * @param signingData The encoded state data (without channelId or signatures) * @param signature Encoded as abi.encode(SessionKeyAuthorization, bytes signature) @@ -70,13 +76,13 @@ contract SessionKeyValidator is ISignatureValidator { bytes calldata signingData, bytes calldata signature, address participant - ) external pure returns (ValidationResult) { + ) external returns (ValidationResult) { (SessionKeyAuthorization memory skAuth, bytes memory skSignature) = abi.decode(signature, (SessionKeyAuthorization, bytes)); // Step 1: Verify participant authorized this session key bytes memory authMessage = toSigningData(skAuth); - bool authResult = EcdsaSignatureUtils.validateEcdsaSigner(authMessage, skAuth.authSignature, participant); + bool authResult = _validateSigner(authMessage, skAuth.authSignature, participant); if (!authResult) { return VALIDATION_FAILURE; @@ -84,10 +90,31 @@ contract SessionKeyValidator is ISignatureValidator { // Step 2: Verify session key signed the full state message bytes memory stateMessage = Utils.pack(channelId, signingData); - if (EcdsaSignatureUtils.validateEcdsaSigner(stateMessage, skSignature, skAuth.sessionKey)) { + if (_validateSigner(stateMessage, skSignature, skAuth.sessionKey)) { return VALIDATION_SUCCESS; } else { return VALIDATION_FAILURE; } } + + /** + * @notice Validates a signature for a given signer (EOA or smart wallet) + * @dev Tries ECDSA validation first, then smart wallet validation + * @param message The message that was signed + * @param signature The signature to validate + * @param signer The expected signer's address + * @return bool True if signature is valid, false otherwise + */ + function _validateSigner(bytes memory message, bytes memory signature, address signer) + private + returns (bool) + { + // Try ECDSA validation first (for EOAs) + if (EcdsaSignatureUtils.validateEcdsaSigner(message, signature, signer)) { + return true; + } + + // Try smart wallet validation (ERC-1271/ERC-6492) + return SwSignatureUtils.validateSmartWalletSigner(message, signature, signer); + } } diff --git a/contracts/test/sigValidators/SessionKeyValidator.t.sol b/contracts/test/sigValidators/SessionKeyValidator.t.sol index 798f259f2..fefdf7562 100644 --- a/contracts/test/sigValidators/SessionKeyValidator.t.sol +++ b/contracts/test/sigValidators/SessionKeyValidator.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.30; import {Test} from "forge-std/Test.sol"; import {TestUtils} from "../TestUtils.sol"; +import {MockSmartWallet, MockSmartWalletFactory, SwTestUtils} from "./SwTestUtils.sol"; import { SessionKeyValidator, @@ -209,3 +210,349 @@ contract SessionKeyValidatorTest_validateSignature is SessionKeyValidatorTest_Ba assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); } } + +/** + * @title SessionKeyValidatorTest_validateSignature_SmartWallet_ERC1271 + * @notice Tests for ERC-1271 signature validation with smart wallets + */ +contract SessionKeyValidatorTest_validateSignature_SmartWallet_ERC1271 is SessionKeyValidatorTest_Base { + MockSmartWallet userSw; + MockSmartWallet sessionKeySw; + + function setUp() public override { + super.setUp(); + userSw = new MockSmartWallet(user); + sessionKeySw = new MockSmartWallet(sessionKey1); + } + + function userSwSign(bytes memory message) internal pure returns (bytes memory) { + return TestUtils.signRaw(vm, USER_PK, message); + } + + function sessionKeySwSign(bytes memory message) internal pure returns (bytes memory) { + return TestUtils.signRaw(vm, SESSION_KEY1_PK, message); + } + + function test_success_participantIsSmartWallet_sessionKeyIsEOA() public { + // Participant (user) is a smart wallet, session key is EOA + bytes memory authMessage = toSigningData( + SessionKeyAuthorization({sessionKey: sessionKey1, metadataHash: METADATA_HASH, authSignature: ""}) + ); + bytes memory authSignature = userSwSign(authMessage); + SessionKeyAuthorization memory skAuth = + SessionKeyAuthorization({sessionKey: sessionKey1, metadataHash: METADATA_HASH, authSignature: authSignature}); + + bytes memory skSignature = signStateWithSk(CHANNEL_ID, SIGNING_DATA, SESSION_KEY1_PK, true); + bytes memory signature = abi.encode(skAuth, skSignature); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, signature, address(userSw)); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_SUCCESS)); + } + + function test_success_participantIsEOA_sessionKeyIsSmartWallet() public { + // Participant is EOA, session key is a smart wallet + bytes memory authMessage = toSigningData( + SessionKeyAuthorization({ + sessionKey: address(sessionKeySw), + metadataHash: METADATA_HASH, + authSignature: "" + }) + ); + bytes memory authSignature = TestUtils.signEip191(vm, USER_PK, authMessage); + SessionKeyAuthorization memory skAuth = SessionKeyAuthorization({ + sessionKey: address(sessionKeySw), + metadataHash: METADATA_HASH, + authSignature: authSignature + }); + + bytes memory stateMessage = Utils.pack(CHANNEL_ID, SIGNING_DATA); + bytes memory skSignature = sessionKeySwSign(stateMessage); + bytes memory signature = abi.encode(skAuth, skSignature); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, signature, user); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_SUCCESS)); + } + + function test_success_bothAreSmartWallets() public { + // Both participant and session key are smart wallets + bytes memory authMessage = toSigningData( + SessionKeyAuthorization({ + sessionKey: address(sessionKeySw), + metadataHash: METADATA_HASH, + authSignature: "" + }) + ); + bytes memory authSignature = userSwSign(authMessage); + SessionKeyAuthorization memory skAuth = SessionKeyAuthorization({ + sessionKey: address(sessionKeySw), + metadataHash: METADATA_HASH, + authSignature: authSignature + }); + + bytes memory stateMessage = Utils.pack(CHANNEL_ID, SIGNING_DATA); + bytes memory skSignature = sessionKeySwSign(stateMessage); + bytes memory signature = abi.encode(skAuth, skSignature); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, signature, address(userSw)); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_SUCCESS)); + } + + function test_failure_participantSmartWallet_wrongAuthSigner() public { + // Participant is a smart wallet, but auth signature is from wrong signer + bytes memory authMessage = toSigningData( + SessionKeyAuthorization({sessionKey: sessionKey1, metadataHash: METADATA_HASH, authSignature: ""}) + ); + // sign by other signer instead of user + bytes memory authSignature = TestUtils.signRaw(vm, OTHER_SIGNER_PK, authMessage); + SessionKeyAuthorization memory skAuth = + SessionKeyAuthorization({sessionKey: sessionKey1, metadataHash: METADATA_HASH, authSignature: authSignature}); + + bytes memory skSignature = signStateWithSk(CHANNEL_ID, SIGNING_DATA, SESSION_KEY1_PK, true); + bytes memory signature = abi.encode(skAuth, skSignature); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, signature, address(userSw)); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); + } + + function test_failure_sessionKeySmartWallet_wrongSessionKeySigner() public { + // Session key is a smart wallet, but state signature is from wrong signer + bytes memory authMessage = toSigningData( + SessionKeyAuthorization({ + sessionKey: address(sessionKeySw), + metadataHash: METADATA_HASH, + authSignature: "" + }) + ); + bytes memory authSignature = TestUtils.signEip191(vm, USER_PK, authMessage); + SessionKeyAuthorization memory skAuth = SessionKeyAuthorization({ + sessionKey: address(sessionKeySw), + metadataHash: METADATA_HASH, + authSignature: authSignature + }); + + bytes memory stateMessage = Utils.pack(CHANNEL_ID, SIGNING_DATA); + // sign by other signer instead of sessionKey1 + bytes memory skSignature = TestUtils.signRaw(vm, SESSION_KEY2_PK, stateMessage); + bytes memory signature = abi.encode(skAuth, skSignature); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, signature, user); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); + } + + function test_failure_participantSmartWallet_walletRejectsSignature() public { + // Participant wallet is configured to reject all signatures + bytes memory authMessage = toSigningData( + SessionKeyAuthorization({sessionKey: sessionKey1, metadataHash: METADATA_HASH, authSignature: ""}) + ); + bytes memory authSignature = TestUtils.signRaw(vm, USER_PK, authMessage); + SessionKeyAuthorization memory skAuth = + SessionKeyAuthorization({sessionKey: sessionKey1, metadataHash: METADATA_HASH, authSignature: authSignature}); + + bytes memory skSignature = signStateWithSk(CHANNEL_ID, SIGNING_DATA, SESSION_KEY1_PK, true); + bytes memory signature = abi.encode(skAuth, skSignature); + + // Make wallet reject all signatures + userSw.setValidation(false); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, signature, address(userSw)); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); + } +} + +/** + * @title SessionKeyValidatorTest_validateSignature_SmartWallet_ERC6492 + * @notice Tests for ERC-6492 signature validation with undeployed smart wallets + */ +contract SessionKeyValidatorTest_validateSignature_SmartWallet_ERC6492 is SessionKeyValidatorTest_Base { + MockSmartWalletFactory factory; + + function setUp() public override { + super.setUp(); + factory = new MockSmartWalletFactory(); + } + + function test_success_participantIsNonDeployedSmartWallet_sessionKeyIsEoa() public { + bytes32 salt = keccak256("user_wallet_salt"); + address userWalletAddress = factory.getAddress(user, salt); + + // Ensure wallet is not deployed + assertEq(userWalletAddress.code.length, 0, "Wallet should not be deployed yet"); + + bytes memory factoryCalldata = abi.encodeWithSelector( + MockSmartWalletFactory.deploy.selector, + user, + salt + ); + + bytes memory authMessage = toSigningData( + SessionKeyAuthorization({sessionKey: sessionKey1, metadataHash: METADATA_HASH, authSignature: ""}) + ); + bytes memory authSig = TestUtils.signRaw(vm, USER_PK, authMessage); + bytes memory authSignature = SwTestUtils.createERC6492Signature(address(factory), factoryCalldata, authSig); + + SessionKeyAuthorization memory skAuth = + SessionKeyAuthorization({sessionKey: sessionKey1, metadataHash: METADATA_HASH, authSignature: authSignature}); + + // Session key signs the state (EOA) + bytes memory skSignature = signStateWithSk(CHANNEL_ID, SIGNING_DATA, SESSION_KEY1_PK, true); + bytes memory signature = abi.encode(skAuth, skSignature); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, signature, userWalletAddress); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_SUCCESS)); + assertTrue(userWalletAddress.code.length > 0, "Wallet should be deployed after validation"); + } + + function test_success_participantIsEoa_sessionKeyIsNonDeployedSmartWallet() public { + bytes32 salt = keccak256("sk_wallet_salt"); + address skWalletAddress = factory.getAddress(sessionKey1, salt); + + // Ensure wallet is not deployed + assertEq(skWalletAddress.code.length, 0, "Wallet should not be deployed yet"); + + // Create factory calldata for deployment + bytes memory factoryCalldata = abi.encodeWithSelector( + MockSmartWalletFactory.deploy.selector, + sessionKey1, + salt + ); + + // Participant (EOA) signs the authorization + bytes memory authMessage = toSigningData( + SessionKeyAuthorization({ + sessionKey: skWalletAddress, + metadataHash: METADATA_HASH, + authSignature: "" + }) + ); + bytes memory authSignature = TestUtils.signEip191(vm, USER_PK, authMessage); + SessionKeyAuthorization memory skAuth = SessionKeyAuthorization({ + sessionKey: skWalletAddress, + metadataHash: METADATA_HASH, + authSignature: authSignature + }); + + + bytes memory stateMessage = Utils.pack(CHANNEL_ID, SIGNING_DATA); + bytes memory stateSig = TestUtils.signRaw(vm, SESSION_KEY1_PK, stateMessage); + bytes memory skSignature = SwTestUtils.createERC6492Signature(address(factory), factoryCalldata, stateSig); + bytes memory signature = abi.encode(skAuth, skSignature); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, signature, user); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_SUCCESS)); + assertTrue(skWalletAddress.code.length > 0, "Wallet should be deployed after validation"); + } + + function test_success_bothAreNonDeployedSmartWallets() public { + bytes32 userSalt = keccak256("user_wallet_salt_both"); + bytes32 skSalt = keccak256("sk_wallet_salt_both"); + address userWalletAddress = factory.getAddress(user, userSalt); + address skWalletAddress = factory.getAddress(sessionKey1, skSalt); + + // Ensure wallets are not deployed + assertEq(userWalletAddress.code.length, 0, "User wallet should not be deployed yet"); + assertEq(skWalletAddress.code.length, 0, "SK wallet should not be deployed yet"); + + // Create factory calldata for both + bytes memory userFactoryCalldata = abi.encodeWithSelector( + MockSmartWalletFactory.deploy.selector, + user, + userSalt + ); + bytes memory skFactoryCalldata = abi.encodeWithSelector( + MockSmartWalletFactory.deploy.selector, + sessionKey1, + skSalt + ); + + // Participant smart wallet signs authorization with ERC-6492 + bytes memory authMessage = toSigningData( + SessionKeyAuthorization({ + sessionKey: skWalletAddress, + metadataHash: METADATA_HASH, + authSignature: "" + }) + ); + bytes memory authSig = TestUtils.signRaw(vm, USER_PK, authMessage); + bytes memory authSignature = SwTestUtils.createERC6492Signature(address(factory), userFactoryCalldata, authSig); + + SessionKeyAuthorization memory skAuth = SessionKeyAuthorization({ + sessionKey: skWalletAddress, + metadataHash: METADATA_HASH, + authSignature: authSignature + }); + + // Session key smart wallet signs state with ERC-6492 + bytes memory stateMessage = Utils.pack(CHANNEL_ID, SIGNING_DATA); + bytes memory stateSig = TestUtils.signRaw(vm, SESSION_KEY1_PK, stateMessage); + bytes memory skSignature = SwTestUtils.createERC6492Signature(address(factory), skFactoryCalldata, stateSig); + bytes memory signature = abi.encode(skAuth, skSignature); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, signature, userWalletAddress); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_SUCCESS)); + assertTrue(userWalletAddress.code.length > 0, "User wallet should be deployed after validation"); + assertTrue(skWalletAddress.code.length > 0, "SK wallet should be deployed after validation"); + } + + function test_failure_nonDeployedParticipantWallet_wrongSigner() public { + bytes32 salt = keccak256("user_wallet_wrong_signer"); + address userWalletAddress = factory.getAddress(user, salt); + + bytes memory factoryCalldata = abi.encodeWithSelector( + MockSmartWalletFactory.deploy.selector, + user, + salt + ); + + // Use wrong signer for auth signature + bytes memory authMessage = toSigningData( + SessionKeyAuthorization({sessionKey: sessionKey1, metadataHash: METADATA_HASH, authSignature: ""}) + ); + bytes memory authSig = TestUtils.signRaw(vm, OTHER_SIGNER_PK, authMessage); // Wrong signer + bytes memory authSignature = SwTestUtils.createERC6492Signature(address(factory), factoryCalldata, authSig); + + SessionKeyAuthorization memory skAuth = + SessionKeyAuthorization({sessionKey: sessionKey1, metadataHash: METADATA_HASH, authSignature: authSignature}); + + bytes memory skSignature = signStateWithSk(CHANNEL_ID, SIGNING_DATA, SESSION_KEY1_PK, true); + bytes memory signature = abi.encode(skAuth, skSignature); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, signature, userWalletAddress); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); + assertTrue(userWalletAddress.code.length > 0, "Wallet should be deployed even with invalid signature"); + } + + function test_failure_nonDeployedSessionKeyWallet_wrongSigner() public { + bytes32 salt = keccak256("sk_wallet_wrong_signer"); + address skWalletAddress = factory.getAddress(sessionKey1, salt); + + bytes memory factoryCalldata = abi.encodeWithSelector( + MockSmartWalletFactory.deploy.selector, + sessionKey1, + salt + ); + + // Use wrong signer for state signature + bytes memory authMessage = toSigningData( + SessionKeyAuthorization({ + sessionKey: skWalletAddress, + metadataHash: METADATA_HASH, + authSignature: "" + }) + ); + bytes memory authSignature = TestUtils.signEip191(vm, USER_PK, authMessage); + SessionKeyAuthorization memory skAuth = SessionKeyAuthorization({ + sessionKey: skWalletAddress, + metadataHash: METADATA_HASH, + authSignature: authSignature + }); + + bytes memory stateMessage = Utils.pack(CHANNEL_ID, SIGNING_DATA); + bytes memory stateSig = TestUtils.signRaw(vm, OTHER_SIGNER_PK, stateMessage); // Wrong signer + bytes memory skSignature = SwTestUtils.createERC6492Signature(address(factory), factoryCalldata, stateSig); + bytes memory signature = abi.encode(skAuth, skSignature); + + ValidationResult result = validator.validateSignature(CHANNEL_ID, SIGNING_DATA, signature, user); + assertEq(ValidationResult.unwrap(result), ValidationResult.unwrap(VALIDATION_FAILURE)); + assertTrue(skWalletAddress.code.length > 0, "SK Wallet should be deployed even with invalid signature"); + } +} From 42d91438935d5371341dcd355d23df543890d037 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Tue, 24 Feb 2026 20:45:29 +0100 Subject: [PATCH 6/6] fix(contracts/SessionKeyValidator): optimize to check smart contract sig first --- contracts/src/sigValidators/SessionKeyValidator.sol | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/contracts/src/sigValidators/SessionKeyValidator.sol b/contracts/src/sigValidators/SessionKeyValidator.sol index 4b6a9373b..8de3f04f7 100644 --- a/contracts/src/sigValidators/SessionKeyValidator.sol +++ b/contracts/src/sigValidators/SessionKeyValidator.sol @@ -109,12 +109,11 @@ contract SessionKeyValidator is ISignatureValidator { private returns (bool) { - // Try ECDSA validation first (for EOAs) - if (EcdsaSignatureUtils.validateEcdsaSigner(message, signature, signer)) { - return true; + if (signer.code.length != 0 || SwSignatureUtils.isERC6492Signature(signature)) { + // If signer has code or signature is ERC-6492, treat as smart wallet + return SwSignatureUtils.validateSmartWalletSigner(message, signature, signer); } - // Try smart wallet validation (ERC-1271/ERC-6492) - return SwSignatureUtils.validateSmartWalletSigner(message, signature, signer); + return EcdsaSignatureUtils.validateEcdsaSigner(message, signature, signer); } }