From 32960e55b3f1fca3633dc80646e1193ce54532dd Mon Sep 17 00:00:00 2001 From: KONFeature Date: Tue, 12 Dec 2023 23:46:38 +0100 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20Add=20a=20new=20ECDSA=20Typed?= =?UTF-8?q?=20validator,=20using=20EIP-712=20signature=20to=20validate=20u?= =?UTF-8?q?serOp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/validator/ECDSATypedValidator.sol | 100 ++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/validator/ECDSATypedValidator.sol diff --git a/src/validator/ECDSATypedValidator.sol b/src/validator/ECDSATypedValidator.sol new file mode 100644 index 00000000..928e997c --- /dev/null +++ b/src/validator/ECDSATypedValidator.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {UserOperation} from "I4337/interfaces/UserOperation.sol"; +import {ECDSA} from "solady/utils/ECDSA.sol"; +import {EIP712} from "solady/utils/EIP712.sol"; +import {IKernelValidator} from "../interfaces/IKernelValidator.sol"; +import {ValidationData} from "../common/Types.sol"; +import {SIG_VALIDATION_FAILED} from "../common/Constants.sol"; + +struct ECDSATypedValidatorStorage { + address owner; +} + +/// @author @KONFeature +/// @title ECDSATypedValidator +/// @notice This validator uses the ECDSA curve to validate signatures. +/// @notice It's using EIP-712 format signature to validate user operations signature & classic signature +contract ECDSATypedValidator is IKernelValidator, EIP712 { + /// @notice The type hash used for kernel user op validation + bytes32 constant USER_OP_TYPEHASH = keccak256("AllowUserOp(address owner,address kernelWallet,bytes32 hash)"); + /// @notice The type hash used for kernel signature validation + bytes32 constant SIGNATURE_TYPEHASH = keccak256("AllowSignature(address owner,address kernelWallet,bytes32 hash)"); + + /// @notice Emitted when the owner of a kernel is changed. + event OwnerChanged(address indexed kernel, address oldOwner, address newOwner); + + /// @notice The validator storage of a kernel. + mapping(address kernel => ECDSATypedValidatorStorage validatorStorage) public ecdsaValidatorStorage; + + /// @dev Get the current name & version of the validator, used for the EIP-712 domain separator from Solady + function _domainNameAndVersion() internal pure override returns (string memory, string memory) { + return ("Kernel:ECDSATypedValidator", "1.0.0"); + } + + /// @dev Tell to solady that the current name & version of the validator won't change, so no need to recompute the eip-712 domain separator + function _domainNameAndVersionMayChange() internal pure override returns (bool) { + return false; + } + + /// @dev Enable this validator for a given `kernel` (msg.sender) + function enable(bytes calldata _data) external payable override { + address owner = address(bytes20(_data[0:20])); + address oldOwner = ecdsaValidatorStorage[msg.sender].owner; + ecdsaValidatorStorage[msg.sender].owner = owner; + emit OwnerChanged(msg.sender, oldOwner, owner); + } + + /// @dev Disable this validator for a given `kernel` (msg.sender) + function disable(bytes calldata) external payable override { + delete ecdsaValidatorStorage[msg.sender]; + } + + /// @dev Validate a `_userOp` using a EIP-712 signature, signed by the owner of the kernel account who is the `_userOp` sender + function validateUserOp(UserOperation calldata _userOp, bytes32 _userOpHash, uint256) + external + payable + override + returns (ValidationData validationData) + { + // Get the owner for the given kernel account + address owner = ecdsaValidatorStorage[_userOp.sender].owner; + + // Build the full message hash to check against + bytes32 typedDataHash = + _hashTypedData(keccak256(abi.encode(USER_OP_TYPEHASH, owner, _userOp.sender, _userOpHash))); + + // Validate the typed data hash signature + if (owner == ECDSA.recover(typedDataHash, _userOp.signature)) { + // If that worked, return a valid validation data + return ValidationData.wrap(0); + } + + // If not, return a failed validation data + return SIG_VALIDATION_FAILED; + } + + /// @dev Validate a `_signature` of the `_hash` ofor the given `kernel` (msg.sender) + function validateSignature(bytes32 _hash, bytes calldata signature) public view override returns (ValidationData) { + // Get the owner for the given kernel account + address owner = ecdsaValidatorStorage[msg.sender].owner; + + // Build the full message hash to check against + bytes32 typedDataHash = _hashTypedData(keccak256(abi.encode(SIGNATURE_TYPEHASH, owner, msg.sender, _hash))); + + // Validate the typed data hash signature + if (owner == ECDSA.recover(typedDataHash, signature)) { + // If that worked, return a valid validation data + return ValidationData.wrap(0); + } + + // If not, return a failed validation data + return SIG_VALIDATION_FAILED; + } + + /// @dev Check if the caller is a valid signer for this kernel account + function validCaller(address _caller, bytes calldata) external view override returns (bool) { + return ecdsaValidatorStorage[msg.sender].owner == _caller; + } +} From 676f4521b6c3253410c8673090ffdff910c2865d Mon Sep 17 00:00:00 2001 From: KONFeature Date: Wed, 13 Dec 2023 00:40:19 +0100 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9C=85=20Adding=20unit=20test=20arround?= =?UTF-8?q?=20EcdsaTypedValidator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/validator/ECDSATypedValidator.sol | 30 +++- test/foundry/validator/KernelECDSATyped.t.sol | 156 ++++++++++++++++++ 2 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 test/foundry/validator/KernelECDSATyped.t.sol diff --git a/src/validator/ECDSATypedValidator.sol b/src/validator/ECDSATypedValidator.sol index 928e997c..6c3a754e 100644 --- a/src/validator/ECDSATypedValidator.sol +++ b/src/validator/ECDSATypedValidator.sol @@ -20,13 +20,21 @@ contract ECDSATypedValidator is IKernelValidator, EIP712 { /// @notice The type hash used for kernel user op validation bytes32 constant USER_OP_TYPEHASH = keccak256("AllowUserOp(address owner,address kernelWallet,bytes32 hash)"); /// @notice The type hash used for kernel signature validation - bytes32 constant SIGNATURE_TYPEHASH = keccak256("AllowSignature(address owner,address kernelWallet,bytes32 hash)"); + bytes32 constant SIGNATURE_TYPEHASH = keccak256("KernelSignature(address owner,address kernelWallet,bytes32 hash)"); /// @notice Emitted when the owner of a kernel is changed. event OwnerChanged(address indexed kernel, address oldOwner, address newOwner); + /* -------------------------------------------------------------------------- */ + /* Storage */ + /* -------------------------------------------------------------------------- */ + /// @notice The validator storage of a kernel. - mapping(address kernel => ECDSATypedValidatorStorage validatorStorage) public ecdsaValidatorStorage; + mapping(address kernel => ECDSATypedValidatorStorage validatorStorage) private ecdsaValidatorStorage; + + /* -------------------------------------------------------------------------- */ + /* EIP-712 Methods */ + /* -------------------------------------------------------------------------- */ /// @dev Get the current name & version of the validator, used for the EIP-712 domain separator from Solady function _domainNameAndVersion() internal pure override returns (string memory, string memory) { @@ -38,6 +46,15 @@ contract ECDSATypedValidator is IKernelValidator, EIP712 { return false; } + /// @dev Export the current domain seperator + function getDomainSeperator() public view returns (bytes32) { + return _domainSeparator(); + } + + /* -------------------------------------------------------------------------- */ + /* Kernel validator Methods */ + /* -------------------------------------------------------------------------- */ + /// @dev Enable this validator for a given `kernel` (msg.sender) function enable(bytes calldata _data) external payable override { address owner = address(bytes20(_data[0:20])); @@ -97,4 +114,13 @@ contract ECDSATypedValidator is IKernelValidator, EIP712 { function validCaller(address _caller, bytes calldata) external view override returns (bool) { return ecdsaValidatorStorage[msg.sender].owner == _caller; } + + /* -------------------------------------------------------------------------- */ + /* Public view methods */ + /* -------------------------------------------------------------------------- */ + + /// @dev Get the owner of a given `kernel` + function getOwner(address _kernel) public view returns (address) { + return ecdsaValidatorStorage[_kernel].owner; + } } diff --git a/test/foundry/validator/KernelECDSATyped.t.sol b/test/foundry/validator/KernelECDSATyped.t.sol new file mode 100644 index 00000000..eab18834 --- /dev/null +++ b/test/foundry/validator/KernelECDSATyped.t.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IEntryPoint} from "I4337/interfaces/IEntryPoint.sol"; +import "src/Kernel.sol"; +import "src/validator/ECDSATypedValidator.sol"; +// test artifacts +// test utils +import "forge-std/Test.sol"; +import {ERC4337Utils} from "../utils/ERC4337Utils.sol"; +import {KernelTestBase} from "../KernelTestBase.sol"; +import {TestExecutor} from "../mock/TestExecutor.sol"; +import {TestValidator} from "../mock/TestValidator.sol"; +import {IKernel} from "src/interfaces/IKernel.sol"; + +using ERC4337Utils for IEntryPoint; + +/// @author @KONFeature +/// @title KernelECDSATypedTest +/// @notice Unit test on the Kernel ECDSA typed validator +contract KernelECDSATypedTest is KernelTestBase { + ECDSATypedValidator private ecdsaTypedValidator; + + function setUp() public virtual { + _initialize(); + ecdsaTypedValidator = new ECDSATypedValidator(); + defaultValidator = ecdsaTypedValidator; + _setAddress(); + _setExecutionDetail(); + } + + function test_ignore() external {} + + function _setExecutionDetail() internal virtual override { + executionDetail.executor = address(new TestExecutor()); + executionSig = TestExecutor.doNothing.selector; + executionDetail.validator = new TestValidator(); + } + + function getEnableData() internal view virtual override returns (bytes memory) { + return ""; + } + + function getValidatorSignature(UserOperation memory) internal view virtual override returns (bytes memory) { + return ""; + } + + function getOwners() internal view override returns (address[] memory) { + address[] memory owners = new address[](1); + owners[0] = owner; + return owners; + } + + function getInitializeData() internal view override returns (bytes memory) { + return abi.encodeWithSelector(KernelStorage.initialize.selector, defaultValidator, abi.encodePacked(owner)); + } + + function signUserOp(UserOperation memory op) internal view override returns (bytes memory) { + return abi.encodePacked(bytes4(0x00000000), _generateUserOpSignature(entryPoint, op, ownerKey)); + } + + function getWrongSignature(UserOperation memory op) internal view override returns (bytes memory) { + return abi.encodePacked(bytes4(0x00000000), _generateUserOpSignature(entryPoint, op, ownerKey + 1)); + } + + function signHash(bytes32 _hash) internal view override returns (bytes memory) { + return _generateHashSignature(_hash, owner, address(kernel), ownerKey); + } + + function getWrongSignature(bytes32 _hash) internal view override returns (bytes memory) { + return _generateHashSignature(_hash, owner, address(kernel), ownerKey + 1); + } + + function test_default_validator_enable() external override { + UserOperation memory op = buildUserOperation( + abi.encodeWithSelector( + IKernel.execute.selector, + address(defaultValidator), + 0, + abi.encodeWithSelector(ECDSATypedValidator.enable.selector, abi.encodePacked(address(0xdeadbeef))), + Operation.Call + ) + ); + performUserOperationWithSig(op); + address owner = ecdsaTypedValidator.getOwner(address(kernel)); + assertEq(owner, address(0xdeadbeef), "owner should be 0xdeadbeef"); + } + + function test_default_validator_disable() external override { + UserOperation memory op = buildUserOperation( + abi.encodeWithSelector( + IKernel.execute.selector, + address(defaultValidator), + 0, + abi.encodeWithSelector(ECDSATypedValidator.disable.selector, ""), + Operation.Call + ) + ); + performUserOperationWithSig(op); + address owner = ecdsaTypedValidator.getOwner(address(kernel)); + assertEq(owner, address(0), "owner should be 0"); + } + + /* -------------------------------------------------------------------------- */ + /* Helper methods */ + /* -------------------------------------------------------------------------- */ + + /// @notice The type hash used for kernel user op validation + bytes32 constant USER_OP_TYPEHASH = keccak256("AllowUserOp(address owner,address kernelWallet,bytes32 hash)"); + + /// @dev Generate the signature for a user op + function _generateUserOpSignature(IEntryPoint _entryPoint, UserOperation memory _op, uint256 _privateKey) + internal + view + returns (bytes memory) + { + // Get the kernel private key owner address + address owner = vm.addr(_privateKey); + + // Get the user op hash + bytes32 userOpHash = _entryPoint.getUserOpHash(_op); + + // Get the validator domain separator + bytes32 domainSeparator = ecdsaTypedValidator.getDomainSeperator(); + bytes32 typedMsgHash = keccak256( + abi.encodePacked( + "\x19\x01", domainSeparator, keccak256(abi.encode(USER_OP_TYPEHASH, owner, _op.sender, userOpHash)) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedMsgHash); + return abi.encodePacked(r, s, v); + } + + /// @notice The type hash used for kernel signature validation + bytes32 constant SIGNATURE_TYPEHASH = keccak256("KernelSignature(address owner,address kernelWallet,bytes32 hash)"); + + /// @dev Generate the signature for a given hash for a kernel account + function _generateHashSignature(bytes32 _hash, address _owner, address _kernel, uint256 _privateKey) + internal + view + returns (bytes memory) + { + // Get the kernel private key owner address + address owner = vm.addr(_privateKey); + + // Get the validator domain separator + bytes32 domainSeparator = ecdsaTypedValidator.getDomainSeperator(); + bytes32 typedMsgHash = keccak256( + abi.encodePacked( + "\x19\x01", domainSeparator, keccak256(abi.encode(SIGNATURE_TYPEHASH, _owner, _kernel, _hash)) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedMsgHash); + return abi.encodePacked(r, s, v); + } +} From 9efcc05faa863f712d587ed31d23b5e6f0e3fa35 Mon Sep 17 00:00:00 2001 From: KONFeature Date: Wed, 13 Dec 2023 09:30:29 +0100 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Rename=20hash=20->=20u?= =?UTF-8?q?serOpHash=20in=20AllowUserOp=20sig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/validator/ECDSATypedValidator.sol | 7 +++---- test/foundry/validator/KernelECDSATyped.t.sol | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/validator/ECDSATypedValidator.sol b/src/validator/ECDSATypedValidator.sol index 6c3a754e..f2dea0c3 100644 --- a/src/validator/ECDSATypedValidator.sol +++ b/src/validator/ECDSATypedValidator.sol @@ -18,12 +18,12 @@ struct ECDSATypedValidatorStorage { /// @notice It's using EIP-712 format signature to validate user operations signature & classic signature contract ECDSATypedValidator is IKernelValidator, EIP712 { /// @notice The type hash used for kernel user op validation - bytes32 constant USER_OP_TYPEHASH = keccak256("AllowUserOp(address owner,address kernelWallet,bytes32 hash)"); + bytes32 constant USER_OP_TYPEHASH = keccak256("AllowUserOp(address owner,address kernelWallet,bytes32 userOpHash)"); /// @notice The type hash used for kernel signature validation bytes32 constant SIGNATURE_TYPEHASH = keccak256("KernelSignature(address owner,address kernelWallet,bytes32 hash)"); /// @notice Emitted when the owner of a kernel is changed. - event OwnerChanged(address indexed kernel, address oldOwner, address newOwner); + event OwnerChanged(address indexed kernel, address newOwner); /* -------------------------------------------------------------------------- */ /* Storage */ @@ -58,9 +58,8 @@ contract ECDSATypedValidator is IKernelValidator, EIP712 { /// @dev Enable this validator for a given `kernel` (msg.sender) function enable(bytes calldata _data) external payable override { address owner = address(bytes20(_data[0:20])); - address oldOwner = ecdsaValidatorStorage[msg.sender].owner; ecdsaValidatorStorage[msg.sender].owner = owner; - emit OwnerChanged(msg.sender, oldOwner, owner); + emit OwnerChanged(msg.sender, owner); } /// @dev Disable this validator for a given `kernel` (msg.sender) diff --git a/test/foundry/validator/KernelECDSATyped.t.sol b/test/foundry/validator/KernelECDSATyped.t.sol index eab18834..cfe3eca5 100644 --- a/test/foundry/validator/KernelECDSATyped.t.sol +++ b/test/foundry/validator/KernelECDSATyped.t.sol @@ -17,7 +17,7 @@ using ERC4337Utils for IEntryPoint; /// @author @KONFeature /// @title KernelECDSATypedTest -/// @notice Unit test on the Kernel ECDSA typed validator +/// @notice Unit test on the Kernel ECDSA typed validator contract KernelECDSATypedTest is KernelTestBase { ECDSATypedValidator private ecdsaTypedValidator; @@ -106,7 +106,7 @@ contract KernelECDSATypedTest is KernelTestBase { /* -------------------------------------------------------------------------- */ /// @notice The type hash used for kernel user op validation - bytes32 constant USER_OP_TYPEHASH = keccak256("AllowUserOp(address owner,address kernelWallet,bytes32 hash)"); + bytes32 constant USER_OP_TYPEHASH = keccak256("AllowUserOp(address owner,address kernelWallet,bytes32 userOpHash)"); /// @dev Generate the signature for a user op function _generateUserOpSignature(IEntryPoint _entryPoint, UserOperation memory _op, uint256 _privateKey)