-
Notifications
You must be signed in to change notification settings - Fork 75
✨ Introduce a new Ecdsa Validator, using EIP-712 as signature #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
leekt
merged 3 commits into
zerodevapp:dev
from
KONFeature:feature/ecdsa-typed-validator
Dec 13, 2023
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this naming convention :)