Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions src/validator/ECDSATypedValidator.sol
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");
Copy link
Collaborator

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 :)

}

/// @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;
}
}
156 changes: 156 additions & 0 deletions test/foundry/validator/KernelECDSATyped.t.sol
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);
}
}