From 6b24bef5d5c6fe03f1c747d241ddaefc7b72850b Mon Sep 17 00:00:00 2001 From: highskore Date: Fri, 11 Jul 2025 15:23:00 +0100 Subject: [PATCH 1/4] feat: StatelessValidatorMultiPlexer prototype --- foundry.toml | 2 +- .../StatelessValidatorMultiPlexer.sol | 91 +++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 src/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol diff --git a/foundry.toml b/foundry.toml index f4278b4..cc109a8 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,5 +1,5 @@ [profile.default] -solc = "0.8.25" +solc = "0.8.28" evm_version = "cancun" src = "src" out = "out" diff --git a/src/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol b/src/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol new file mode 100644 index 0000000..2a456a5 --- /dev/null +++ b/src/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.28; + +// Contracts +import { ERC7579StatelessValidatorBase } from "modulekit/Modules.sol"; + +// Constants +import { MODULE_TYPE_STATELESS_VALIDATOR } from "modulekit/module-bases/utils/ERC7579Constants.sol"; + +/** + * @title StatelessValidatorMultiPlexer + * @dev A stateless validator multiplexer that allows multiple stateless validators to be used in a + * single module. + * @author highskore + */ +contract StatelessValidatorMultiPlexer is ERC7579StatelessValidatorBase { + /*////////////////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////////////////*/ + + error MismatchedValidatorsAndDataLength(); + + /*////////////////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Mapping of smart account address to initialized state + mapping(address account => bool initialized) public isInitialized; + + /*////////////////////////////////////////////////////////////////////////// + CONFIG + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Called when the module is installed on a smart account + function onInstall(bytes calldata) external override { } + + /// @notice Called when the module is uninstalled from a smart account + function onUninstall(bytes calldata) external override { } + + /// @notice Returns the type of the module + /// @dev Implements interface to indicate validator capabilities + /// @param typeID Type identifier to check + /// @return bool True if this module supports the specified type + function isModuleType(uint256 typeID) external pure override returns (bool) { + return typeID == MODULE_TYPE_STATELESS_VALIDATOR; + } + + /*////////////////////////////////////////////////////////////////////////// + VALIDATION + //////////////////////////////////////////////////////////////////////////*/ + + /// TODO: Optimize this + + /// @notice Validates a signature with data by multiplexing through stateless validators + /// @param hash The hash of the data to validate + /// @param signature The signature to validate + /// @param data The data to validate against the signature, + /// the data is encoded in the following format: + /// abi.encode(address[] validators, bytes[] data) + /// @return bool True if the signature is valid for all validators + function validateSignatureWithData( + bytes32 hash, + bytes calldata signature, + bytes calldata data + ) + external + view + override + returns (bool) + { + // Decode the data to get the list of validators and their corresponding data + (address[] memory validators, bytes[] memory validatorData) = + abi.decode(data, (address[], bytes[])); + + // Ensure the number of validators matches the number of data entries + require(validators.length == validatorData.length, MismatchedValidatorsAndDataLength()); + + // Validate each signature with its corresponding data + for (uint256 i = 0; i < validators.length; i++) { + // Call the validateSignatureWithData function on each validator + bool validSig = ERC7579StatelessValidatorBase(validators[i]).validateSignatureWithData( + hash, signature, validatorData[i] + ); + if (!validSig) { + return false; // If any validation fails, return false + } + } + + return true; // All validations passed + } +} From 8d4b36b15658b6a9207f65b02ee34b43b4dc52c5 Mon Sep 17 00:00:00 2001 From: highskore Date: Fri, 18 Jul 2025 02:47:26 +0200 Subject: [PATCH 2/4] test(StatelessValidatorMultiPlexer): add unit tests --- .vscode/settings.json | 3 + .../StatelessValidatorMultiPlexer.sol | 17 +- .../StatelessValidatorMultiPlexer.sol | 423 ++++++++++++++++++ 3 files changed, 439 insertions(+), 4 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 test/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f3e5c0d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "solidity.compileUsingRemoteVersion": "v0.8.28+commit.7893614a" +} diff --git a/src/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol b/src/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol index 2a456a5..86aec40 100644 --- a/src/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol +++ b/src/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol @@ -53,7 +53,7 @@ contract StatelessValidatorMultiPlexer is ERC7579StatelessValidatorBase { /// @notice Validates a signature with data by multiplexing through stateless validators /// @param hash The hash of the data to validate - /// @param signature The signature to validate + /// @param signature The signatures to validate /// @param data The data to validate against the signature, /// the data is encoded in the following format: /// abi.encode(address[] validators, bytes[] data) @@ -68,18 +68,27 @@ contract StatelessValidatorMultiPlexer is ERC7579StatelessValidatorBase { override returns (bool) { + // Decode the signatures array + bytes[] memory signatures = abi.decode(signature, (bytes[])); + // Decode the data to get the list of validators and their corresponding data (address[] memory validators, bytes[] memory validatorData) = abi.decode(data, (address[], bytes[])); + // Cache the length of the validators array + uint256 validatorsLength = validators.length; + // Ensure the number of validators matches the number of data entries - require(validators.length == validatorData.length, MismatchedValidatorsAndDataLength()); + require( + ((validatorsLength == validatorData.length) && (validatorsLength == signatures.length)), + MismatchedValidatorsAndDataLength() + ); // Validate each signature with its corresponding data - for (uint256 i = 0; i < validators.length; i++) { + for (uint256 i = 0; i < validatorsLength; i++) { // Call the validateSignatureWithData function on each validator bool validSig = ERC7579StatelessValidatorBase(validators[i]).validateSignatureWithData( - hash, signature, validatorData[i] + hash, signatures[i], validatorData[i] ); if (!validSig) { return false; // If any validation fails, return false diff --git a/test/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol b/test/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol new file mode 100644 index 0000000..ffc966e --- /dev/null +++ b/test/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol @@ -0,0 +1,423 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { BaseTest } from "test/Base.t.sol"; +import { StatelessValidatorMultiPlexer } from + "src/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol"; +import { OwnableValidator } from "src/OwnableValidator/OwnableValidator.sol"; +import { WebAuthnValidator } from "src/WebAuthnValidator/WebAuthnValidator.sol"; +import { WebAuthn } from "webauthn-sol/src/WebAuthn.sol"; +import { signHash } from "test/utils/Signature.sol"; +import { Base64Url } from "FreshCryptoLib/utils/Base64Url.sol"; +import { LibSort } from "solady/utils/LibSort.sol"; + +contract StatelessValidatorMultiPlexerTest is BaseTest { + using LibSort for *; + + /*////////////////////////////////////////////////////////////////////////// + CONTRACTS + //////////////////////////////////////////////////////////////////////////*/ + + StatelessValidatorMultiPlexer internal multiplexer; + OwnableValidator internal ownableValidator; + WebAuthnValidator internal webAuthnValidator; + + /*////////////////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + uint256 ownableThreshold = 2; + address[] owners; + uint256[] ownerPks; + + uint256 webAuthnThreshold = 1; + uint256[] pubKeysX; + uint256[] pubKeysY; + bool[] requireUVs; + bytes32[] credentialIds; + + WebAuthn.WebAuthnAuth mockAuth; + bytes mockWebAuthnSignature; + bytes mockOwnableSignature; + + /*////////////////////////////////////////////////////////////////////////// + SETUP + //////////////////////////////////////////////////////////////////////////*/ + + function setUp() public virtual override { + BaseTest.setUp(); + + multiplexer = new StatelessValidatorMultiPlexer(); + ownableValidator = new OwnableValidator(); + webAuthnValidator = new WebAuthnValidator(); + + _setupOwnableData(); + _setupWebAuthnData(); + } + + function _setupOwnableData() internal { + owners = new address[](2); + ownerPks = new uint256[](2); + + (address owner1, uint256 owner1Pk) = makeAddrAndKey("owner1"); + owners[0] = owner1; + ownerPks[0] = owner1Pk; + + (address owner2, uint256 owner2Pk) = makeAddrAndKey("owner2"); + + uint256 counter = 0; + while (uint160(owner1) > uint160(owner2)) { + counter++; + (owner2, owner2Pk) = makeAddrAndKey(vm.toString(counter)); + } + owners[1] = owner2; + ownerPks[1] = owner2Pk; + + bytes32 hash = createTestHash(); + bytes memory signature1 = signHash(ownerPks[0], hash); + bytes memory signature2 = signHash(ownerPks[1], hash); + mockOwnableSignature = abi.encodePacked(signature1, signature2); + } + + function _setupWebAuthnData() internal { + pubKeysX = new uint256[](1); + pubKeysY = new uint256[](1); + requireUVs = new bool[](1); + credentialIds = new bytes32[](1); + + pubKeysX[0] = + 66_296_829_923_831_658_891_499_717_579_803_548_012_279_830_557_731_564_719_736_971_029_660_387_468_805; + pubKeysY[0] = + 46_098_569_798_045_992_993_621_049_610_647_226_011_837_333_919_273_603_402_527_314_962_291_506_652_186; + requireUVs[0] = false; + + credentialIds[0] = webAuthnValidator.generateCredentialId( + pubKeysX[0], pubKeysY[0], requireUVs[0], address(this) + ); + + bytes memory challenge = abi.encode(createTestHash()); + + mockAuth = WebAuthn.WebAuthnAuth({ + authenticatorData: hex"49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630100000001", + clientDataJSON: string.concat( + '{"type":"webauthn.get","challenge":"', + Base64Url.encode(challenge), + '","origin":"http://localhost:8080","crossOrigin":false}' + ), + challengeIndex: 23, + typeIndex: 1, + r: 23_510_924_181_331_275_540_501_876_269_042_668_160_690_304_423_490_805_737_085_519_687_669_896_593_880, + s: 36_590_747_517_247_563_381_084_733_394_442_750_806_324_326_036_343_798_276_847_517_765_557_371_045_088 + }); + + WebAuthn.WebAuthnAuth[] memory sigs = new WebAuthn.WebAuthnAuth[](1); + sigs[0] = mockAuth; + mockWebAuthnSignature = abi.encode(sigs); + } + + /*////////////////////////////////////////////////////////////////////////// + TESTS + //////////////////////////////////////////////////////////////////////////*/ + + function test_OnInstall() public { + multiplexer.onInstall(""); + assertTrue(true); + } + + function test_OnUninstall() public { + multiplexer.onUninstall(""); + assertTrue(true); + } + + function test_IsModuleTypeWhenTypeIdIs7() public view { + bool isModuleType = multiplexer.isModuleType(7); + assertTrue(isModuleType); + } + + function test_IsModuleTypeWhenTypeIdIsNot7() public view { + bool isModuleType = multiplexer.isModuleType(1); + assertFalse(isModuleType); + } + + function test_ValidateSignatureWithDataRevertWhen_ValidatorsAndDataLengthMismatch() public { + bytes32 hash = createTestHash(); + + address[] memory validators = new address[](2); + validators[0] = address(ownableValidator); + validators[1] = address(webAuthnValidator); + + bytes[] memory validatorData = new bytes[](1); + validatorData[0] = abi.encode(ownableThreshold, owners); + + bytes memory data = abi.encode(validators, validatorData); + + bytes[] memory signatures = new bytes[](2); + signatures[0] = mockOwnableSignature; + signatures[1] = mockWebAuthnSignature; + bytes memory signature = abi.encode(signatures); + + vm.expectRevert(StatelessValidatorMultiPlexer.MismatchedValidatorsAndDataLength.selector); + multiplexer.validateSignatureWithData(hash, signature, data); + } + + function test_ValidateSignatureWithDataRevertWhen_ValidatorsAndSignaturesLengthMismatch() + public + { + bytes32 hash = createTestHash(); + + address[] memory validators = new address[](2); + validators[0] = address(ownableValidator); + validators[1] = address(webAuthnValidator); + + bytes[] memory validatorData = new bytes[](2); + validatorData[0] = abi.encode(ownableThreshold, owners); + + WebAuthnValidator.WebAuthnCredential[] memory webAuthnCredentials = + new WebAuthnValidator.WebAuthnCredential[](1); + webAuthnCredentials[0] = WebAuthnValidator.WebAuthnCredential({ + pubKeyX: pubKeysX[0], + pubKeyY: pubKeysY[0], + requireUV: requireUVs[0] + }); + + WebAuthnValidator.WebAuthVerificationContext memory context = WebAuthnValidator + .WebAuthVerificationContext({ + usePrecompile: false, + threshold: webAuthnThreshold, + credentialIds: credentialIds, + credentialData: webAuthnCredentials + }); + validatorData[1] = abi.encode(context); + + bytes memory data = abi.encode(validators, validatorData); + + bytes[] memory signatures = new bytes[](1); + signatures[0] = mockOwnableSignature; + bytes memory signature = abi.encode(signatures); + + vm.expectRevert(StatelessValidatorMultiPlexer.MismatchedValidatorsAndDataLength.selector); + multiplexer.validateSignatureWithData(hash, signature, data); + } + + function test_ValidateSignatureWithDataWhen_SingleValidatorFails() public { + bytes32 hash = createTestHash(); + + address[] memory validators = new address[](1); + validators[0] = address(ownableValidator); + + bytes[] memory validatorData = new bytes[](1); + validatorData[0] = abi.encode(ownableThreshold, owners); + + bytes memory data = abi.encode(validators, validatorData); + bytes memory invalidSignature = + abi.encodePacked(signHash(uint256(1), hash), signHash(uint256(2), hash)); + + bytes[] memory signatures = new bytes[](1); + signatures[0] = invalidSignature; + bytes memory signature = abi.encode(signatures); + + bool isValid = multiplexer.validateSignatureWithData(hash, signature, data); + assertFalse(isValid); + } + + function test_ValidateSignatureWithDataWhen_SingleValidatorSucceeds() public view { + bytes32 hash = createTestHash(); + + address[] memory validators = new address[](1); + validators[0] = address(ownableValidator); + + bytes[] memory validatorData = new bytes[](1); + validatorData[0] = abi.encode(ownableThreshold, owners); + + bytes memory data = abi.encode(validators, validatorData); + + bytes[] memory signatures = new bytes[](1); + signatures[0] = mockOwnableSignature; + + bool isValid = multiplexer.validateSignatureWithData(hash, abi.encode(signatures), data); + assertTrue(isValid); + } + + function test_ValidateSignatureWithDataWhen_MultipleValidatorsAllSucceed() public view { + bytes32 hash = createTestHash(); + + address[] memory validators = new address[](2); + validators[0] = address(ownableValidator); + validators[1] = address(webAuthnValidator); + + bytes[] memory validatorData = new bytes[](2); + validatorData[0] = abi.encode(ownableThreshold, owners); + + WebAuthnValidator.WebAuthnCredential[] memory webAuthnCredentials = + new WebAuthnValidator.WebAuthnCredential[](1); + webAuthnCredentials[0] = WebAuthnValidator.WebAuthnCredential({ + pubKeyX: pubKeysX[0], + pubKeyY: pubKeysY[0], + requireUV: requireUVs[0] + }); + + WebAuthnValidator.WebAuthVerificationContext memory context = WebAuthnValidator + .WebAuthVerificationContext({ + usePrecompile: false, + threshold: webAuthnThreshold, + credentialIds: credentialIds, + credentialData: webAuthnCredentials + }); + validatorData[1] = abi.encode(context); + + bytes memory data = abi.encode(validators, validatorData); + + bytes[] memory signatures = new bytes[](2); + signatures[0] = mockOwnableSignature; + signatures[1] = mockWebAuthnSignature; + bytes memory signature = abi.encode(signatures); + + bool isValid = multiplexer.validateSignatureWithData(hash, signature, data); + assertTrue(isValid); + } + + function test_ValidateSignatureWithDataWhen_FirstValidatorFailsSecondSucceeds() public { + bytes32 hash = createTestHash(); + + address[] memory validators = new address[](2); + validators[0] = address(ownableValidator); + validators[1] = address(webAuthnValidator); + + bytes[] memory validatorData = new bytes[](2); + validatorData[0] = abi.encode(ownableThreshold, owners); + + WebAuthnValidator.WebAuthnCredential[] memory webAuthnCredentials = + new WebAuthnValidator.WebAuthnCredential[](1); + webAuthnCredentials[0] = WebAuthnValidator.WebAuthnCredential({ + pubKeyX: pubKeysX[0], + pubKeyY: pubKeysY[0], + requireUV: requireUVs[0] + }); + + WebAuthnValidator.WebAuthVerificationContext memory context = WebAuthnValidator + .WebAuthVerificationContext({ + usePrecompile: false, + threshold: webAuthnThreshold, + credentialIds: credentialIds, + credentialData: webAuthnCredentials + }); + validatorData[1] = abi.encode(context); + + bytes memory data = abi.encode(validators, validatorData); + bytes memory invalidOwnableSignature = + abi.encodePacked(signHash(uint256(1), hash), signHash(uint256(2), hash)); + + bytes[] memory signatures = new bytes[](2); + signatures[0] = invalidOwnableSignature; + signatures[1] = mockWebAuthnSignature; + bytes memory signature = abi.encode(signatures); + + bool isValid = multiplexer.validateSignatureWithData(hash, signature, data); + assertFalse(isValid); + } + + function test_ValidateSignatureWithDataWhen_FirstValidatorSucceedsSecondFails() public view { + bytes32 hash = createTestHash(); + + address[] memory validators = new address[](2); + validators[0] = address(ownableValidator); + validators[1] = address(webAuthnValidator); + + bytes[] memory validatorData = new bytes[](2); + validatorData[0] = abi.encode(ownableThreshold, owners); + + WebAuthnValidator.WebAuthnCredential[] memory webAuthnCredentials = + new WebAuthnValidator.WebAuthnCredential[](1); + webAuthnCredentials[0] = WebAuthnValidator.WebAuthnCredential({ + pubKeyX: 99_999, + pubKeyY: 88_888, + requireUV: false + }); + + WebAuthnValidator.WebAuthVerificationContext memory context = WebAuthnValidator + .WebAuthVerificationContext({ + usePrecompile: false, + threshold: webAuthnThreshold, + credentialIds: new bytes32[](1), + credentialData: webAuthnCredentials + }); + validatorData[1] = abi.encode(context); + + bytes memory data = abi.encode(validators, validatorData); + + bytes[] memory signatures = new bytes[](2); + signatures[0] = mockOwnableSignature; + signatures[1] = mockWebAuthnSignature; + bytes memory signature = abi.encode(signatures); + + bool isValid = multiplexer.validateSignatureWithData(hash, signature, data); + assertFalse(isValid); + } + + function test_ValidateSignatureWithDataWhen_NoValidators() public view { + bytes32 hash = createTestHash(); + + address[] memory validators = new address[](0); + bytes[] memory validatorData = new bytes[](0); + + bytes memory data = abi.encode(validators, validatorData); + + bytes[] memory signatures = new bytes[](0); + bytes memory signature = abi.encode(signatures); + + bool isValid = multiplexer.validateSignatureWithData(hash, signature, data); + assertTrue(isValid); + } + + function test_ValidateSignatureWithDataWhen_ThreeValidatorsAllSucceed() public { + bytes32 hash = createTestHash(); + + OwnableValidator anotherOwnableValidator = new OwnableValidator(); + + address[] memory validators = new address[](3); + validators[0] = address(ownableValidator); + validators[1] = address(webAuthnValidator); + validators[2] = address(anotherOwnableValidator); + + bytes[] memory validatorData = new bytes[](3); + validatorData[0] = abi.encode(ownableThreshold, owners); + + WebAuthnValidator.WebAuthnCredential[] memory webAuthnCredentials = + new WebAuthnValidator.WebAuthnCredential[](1); + webAuthnCredentials[0] = WebAuthnValidator.WebAuthnCredential({ + pubKeyX: pubKeysX[0], + pubKeyY: pubKeysY[0], + requireUV: requireUVs[0] + }); + + WebAuthnValidator.WebAuthVerificationContext memory context = WebAuthnValidator + .WebAuthVerificationContext({ + usePrecompile: false, + threshold: webAuthnThreshold, + credentialIds: credentialIds, + credentialData: webAuthnCredentials + }); + validatorData[1] = abi.encode(context); + + validatorData[2] = abi.encode(1, owners); + + bytes memory data = abi.encode(validators, validatorData); + + bytes[] memory signatures = new bytes[](3); + signatures[0] = mockOwnableSignature; + signatures[1] = mockWebAuthnSignature; + signatures[2] = mockOwnableSignature; + bytes memory signature = abi.encode(signatures); + + bool isValid = multiplexer.validateSignatureWithData(hash, signature, data); + assertTrue(isValid); + } + + /*////////////////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + function createTestHash() internal pure returns (bytes32) { + return bytes32(0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf); + } +} From 4d765af3802534b4b839fd4e9ab237803f2203c7 Mon Sep 17 00:00:00 2001 From: highskore Date: Mon, 21 Jul 2025 10:43:53 +0200 Subject: [PATCH 3/4] chore: remove comment --- .../StatelessValidatorMultiPlexer.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol b/src/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol index 86aec40..6a16135 100644 --- a/src/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol +++ b/src/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol @@ -49,8 +49,6 @@ contract StatelessValidatorMultiPlexer is ERC7579StatelessValidatorBase { VALIDATION //////////////////////////////////////////////////////////////////////////*/ - /// TODO: Optimize this - /// @notice Validates a signature with data by multiplexing through stateless validators /// @param hash The hash of the data to validate /// @param signature The signatures to validate From 6240a444374255ee75c2fe5cccc0d2c31611be04 Mon Sep 17 00:00:00 2001 From: highskore Date: Tue, 22 Jul 2025 10:45:20 +0200 Subject: [PATCH 4/4] feat(StatelessValidatorMultiPlexer): add threshold --- .../StatelessValidatorMultiPlexer.sol | 33 +++--- ...ol => StatelessValidatorMultiPlexer.t.sol} | 100 +++++++++++++++--- 2 files changed, 105 insertions(+), 28 deletions(-) rename test/StatelessValidatorMultiPlexer/{StatelessValidatorMultiPlexer.sol => StatelessValidatorMultiPlexer.t.sol} (83%) diff --git a/src/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol b/src/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol index 6a16135..63c5d8d 100644 --- a/src/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol +++ b/src/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol @@ -20,13 +20,6 @@ contract StatelessValidatorMultiPlexer is ERC7579StatelessValidatorBase { error MismatchedValidatorsAndDataLength(); - /*////////////////////////////////////////////////////////////////////////// - STORAGE - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Mapping of smart account address to initialized state - mapping(address account => bool initialized) public isInitialized; - /*////////////////////////////////////////////////////////////////////////// CONFIG //////////////////////////////////////////////////////////////////////////*/ @@ -37,6 +30,9 @@ contract StatelessValidatorMultiPlexer is ERC7579StatelessValidatorBase { /// @notice Called when the module is uninstalled from a smart account function onUninstall(bytes calldata) external override { } + /// @notice Checks if the module is initialized for a smart account + function isInitialized(address smartAccount) external view override returns (bool) { } + /// @notice Returns the type of the module /// @dev Implements interface to indicate validator capabilities /// @param typeID Type identifier to check @@ -70,8 +66,13 @@ contract StatelessValidatorMultiPlexer is ERC7579StatelessValidatorBase { bytes[] memory signatures = abi.decode(signature, (bytes[])); // Decode the data to get the list of validators and their corresponding data - (address[] memory validators, bytes[] memory validatorData) = - abi.decode(data, (address[], bytes[])); + (address[] memory validators, bytes[] memory validatorData, uint8 threshold) = + abi.decode(data, (address[], bytes[], uint8)); + + // Ensure the threshold is not zero + if (threshold == 0) { + return false; + } // Cache the length of the validators array uint256 validatorsLength = validators.length; @@ -82,17 +83,25 @@ contract StatelessValidatorMultiPlexer is ERC7579StatelessValidatorBase { MismatchedValidatorsAndDataLength() ); + // Count the number of valid signatures + uint8 validCount = 0; + // Validate each signature with its corresponding data for (uint256 i = 0; i < validatorsLength; i++) { // Call the validateSignatureWithData function on each validator bool validSig = ERC7579StatelessValidatorBase(validators[i]).validateSignatureWithData( hash, signatures[i], validatorData[i] ); - if (!validSig) { - return false; // If any validation fails, return false + if (validSig) { + validCount++; } } - return true; // All validations passed + if (validCount >= threshold) { + // If we have enough valid signatures, we can return true + return true; + } + // If we do not have enough valid signatures, return false + return false; } } diff --git a/test/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol b/test/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.t.sol similarity index 83% rename from test/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol rename to test/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.t.sol index ffc966e..2f789be 100644 --- a/test/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.sol +++ b/test/StatelessValidatorMultiPlexer/StatelessValidatorMultiPlexer.t.sol @@ -149,7 +149,8 @@ contract StatelessValidatorMultiPlexerTest is BaseTest { bytes[] memory validatorData = new bytes[](1); validatorData[0] = abi.encode(ownableThreshold, owners); - bytes memory data = abi.encode(validators, validatorData); + uint8 threshold = 2; + bytes memory data = abi.encode(validators, validatorData, threshold); bytes[] memory signatures = new bytes[](2); signatures[0] = mockOwnableSignature; @@ -189,7 +190,8 @@ contract StatelessValidatorMultiPlexerTest is BaseTest { }); validatorData[1] = abi.encode(context); - bytes memory data = abi.encode(validators, validatorData); + uint8 threshold = 2; + bytes memory data = abi.encode(validators, validatorData, threshold); bytes[] memory signatures = new bytes[](1); signatures[0] = mockOwnableSignature; @@ -199,6 +201,26 @@ contract StatelessValidatorMultiPlexerTest is BaseTest { multiplexer.validateSignatureWithData(hash, signature, data); } + function test_ValidateSignatureWithDataWhen_ZeroThreshold() public view { + bytes32 hash = createTestHash(); + + address[] memory validators = new address[](1); + validators[0] = address(ownableValidator); + + bytes[] memory validatorData = new bytes[](1); + validatorData[0] = abi.encode(ownableThreshold, owners); + + uint8 threshold = 0; + bytes memory data = abi.encode(validators, validatorData, threshold); + + bytes[] memory signatures = new bytes[](1); + signatures[0] = mockOwnableSignature; + bytes memory signature = abi.encode(signatures); + + bool isValid = multiplexer.validateSignatureWithData(hash, signature, data); + assertFalse(isValid); + } + function test_ValidateSignatureWithDataWhen_SingleValidatorFails() public { bytes32 hash = createTestHash(); @@ -208,7 +230,8 @@ contract StatelessValidatorMultiPlexerTest is BaseTest { bytes[] memory validatorData = new bytes[](1); validatorData[0] = abi.encode(ownableThreshold, owners); - bytes memory data = abi.encode(validators, validatorData); + uint8 threshold = 1; + bytes memory data = abi.encode(validators, validatorData, threshold); bytes memory invalidSignature = abi.encodePacked(signHash(uint256(1), hash), signHash(uint256(2), hash)); @@ -229,7 +252,8 @@ contract StatelessValidatorMultiPlexerTest is BaseTest { bytes[] memory validatorData = new bytes[](1); validatorData[0] = abi.encode(ownableThreshold, owners); - bytes memory data = abi.encode(validators, validatorData); + uint8 threshold = 1; + bytes memory data = abi.encode(validators, validatorData, threshold); bytes[] memory signatures = new bytes[](1); signatures[0] = mockOwnableSignature; @@ -265,7 +289,8 @@ contract StatelessValidatorMultiPlexerTest is BaseTest { }); validatorData[1] = abi.encode(context); - bytes memory data = abi.encode(validators, validatorData); + uint8 threshold = 2; + bytes memory data = abi.encode(validators, validatorData, threshold); bytes[] memory signatures = new bytes[](2); signatures[0] = mockOwnableSignature; @@ -276,7 +301,7 @@ contract StatelessValidatorMultiPlexerTest is BaseTest { assertTrue(isValid); } - function test_ValidateSignatureWithDataWhen_FirstValidatorFailsSecondSucceeds() public { + function test_ValidateSignatureWithDataWhen_ThresholdOfOne() public view { bytes32 hash = createTestHash(); address[] memory validators = new address[](2); @@ -303,7 +328,47 @@ contract StatelessValidatorMultiPlexerTest is BaseTest { }); validatorData[1] = abi.encode(context); - bytes memory data = abi.encode(validators, validatorData); + uint8 threshold = 1; // Only need one valid signature + bytes memory data = abi.encode(validators, validatorData, threshold); + + bytes[] memory signatures = new bytes[](2); + signatures[0] = mockOwnableSignature; + signatures[1] = mockWebAuthnSignature; + bytes memory signature = abi.encode(signatures); + + bool isValid = multiplexer.validateSignatureWithData(hash, signature, data); + assertTrue(isValid); + } + + function test_ValidateSignatureWithDataWhen_FirstValidatorFailsButThresholdMet() public { + bytes32 hash = createTestHash(); + + address[] memory validators = new address[](2); + validators[0] = address(ownableValidator); + validators[1] = address(webAuthnValidator); + + bytes[] memory validatorData = new bytes[](2); + validatorData[0] = abi.encode(ownableThreshold, owners); + + WebAuthnValidator.WebAuthnCredential[] memory webAuthnCredentials = + new WebAuthnValidator.WebAuthnCredential[](1); + webAuthnCredentials[0] = WebAuthnValidator.WebAuthnCredential({ + pubKeyX: pubKeysX[0], + pubKeyY: pubKeysY[0], + requireUV: requireUVs[0] + }); + + WebAuthnValidator.WebAuthVerificationContext memory context = WebAuthnValidator + .WebAuthVerificationContext({ + usePrecompile: false, + threshold: webAuthnThreshold, + credentialIds: credentialIds, + credentialData: webAuthnCredentials + }); + validatorData[1] = abi.encode(context); + + uint8 threshold = 1; // Only need one valid signature + bytes memory data = abi.encode(validators, validatorData, threshold); bytes memory invalidOwnableSignature = abi.encodePacked(signHash(uint256(1), hash), signHash(uint256(2), hash)); @@ -313,10 +378,10 @@ contract StatelessValidatorMultiPlexerTest is BaseTest { bytes memory signature = abi.encode(signatures); bool isValid = multiplexer.validateSignatureWithData(hash, signature, data); - assertFalse(isValid); + assertTrue(isValid); // Should pass because threshold is 1 and webAuthn succeeds } - function test_ValidateSignatureWithDataWhen_FirstValidatorSucceedsSecondFails() public view { + function test_ValidateSignatureWithDataWhen_NotEnoughValidSignatures() public view { bytes32 hash = createTestHash(); address[] memory validators = new address[](2); @@ -343,7 +408,8 @@ contract StatelessValidatorMultiPlexerTest is BaseTest { }); validatorData[1] = abi.encode(context); - bytes memory data = abi.encode(validators, validatorData); + uint8 threshold = 2; // Need both validators to succeed + bytes memory data = abi.encode(validators, validatorData, threshold); bytes[] memory signatures = new bytes[](2); signatures[0] = mockOwnableSignature; @@ -351,7 +417,7 @@ contract StatelessValidatorMultiPlexerTest is BaseTest { bytes memory signature = abi.encode(signatures); bool isValid = multiplexer.validateSignatureWithData(hash, signature, data); - assertFalse(isValid); + assertFalse(isValid); // Should fail because webAuthn validation fails } function test_ValidateSignatureWithDataWhen_NoValidators() public view { @@ -360,16 +426,17 @@ contract StatelessValidatorMultiPlexerTest is BaseTest { address[] memory validators = new address[](0); bytes[] memory validatorData = new bytes[](0); - bytes memory data = abi.encode(validators, validatorData); + uint8 threshold = 1; + bytes memory data = abi.encode(validators, validatorData, threshold); bytes[] memory signatures = new bytes[](0); bytes memory signature = abi.encode(signatures); bool isValid = multiplexer.validateSignatureWithData(hash, signature, data); - assertTrue(isValid); + assertFalse(isValid); // Should fail because threshold is 1 but no validators } - function test_ValidateSignatureWithDataWhen_ThreeValidatorsAllSucceed() public { + function test_ValidateSignatureWithDataWhen_ThreeValidatorsWithThresholdTwo() public { bytes32 hash = createTestHash(); OwnableValidator anotherOwnableValidator = new OwnableValidator(); @@ -401,7 +468,8 @@ contract StatelessValidatorMultiPlexerTest is BaseTest { validatorData[2] = abi.encode(1, owners); - bytes memory data = abi.encode(validators, validatorData); + uint8 threshold = 2; // Need at least 2 validators to succeed + bytes memory data = abi.encode(validators, validatorData, threshold); bytes[] memory signatures = new bytes[](3); signatures[0] = mockOwnableSignature; @@ -410,7 +478,7 @@ contract StatelessValidatorMultiPlexerTest is BaseTest { bytes memory signature = abi.encode(signatures); bool isValid = multiplexer.validateSignatureWithData(hash, signature, data); - assertTrue(isValid); + assertTrue(isValid); // All 3 validators succeed, threshold of 2 is met } /*//////////////////////////////////////////////////////////////////////////