diff --git a/src/validator/ECDSATypedValidator.sol b/src/validator/ECDSATypedValidator.sol new file mode 100644 index 00000000..f2dea0c3 --- /dev/null +++ b/src/validator/ECDSATypedValidator.sol @@ -0,0 +1,125 @@ +// 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 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 newOwner); + + /* -------------------------------------------------------------------------- */ + /* Storage */ + /* -------------------------------------------------------------------------- */ + + /// @notice The validator storage of a kernel. + 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) { + 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 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])); + ecdsaValidatorStorage[msg.sender].owner = owner; + emit OwnerChanged(msg.sender, 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; + } + + /* -------------------------------------------------------------------------- */ + /* 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..cfe3eca5 --- /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 userOpHash)"); + + /// @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); + } +}