From 4efaf36ad113ee76190671cfd05baa8b78000499 Mon Sep 17 00:00:00 2001 From: leekt Date: Thu, 27 Apr 2023 14:19:19 +0900 Subject: [PATCH 01/18] temp --- src/Kernel.sol | 84 ++++++++++------------------------ src/abstract/KernelStorage.sol | 25 ++++++++++ 2 files changed, 50 insertions(+), 59 deletions(-) diff --git a/src/Kernel.sol b/src/Kernel.sol index 86662fc1..3652455a 100644 --- a/src/Kernel.sol +++ b/src/Kernel.sol @@ -26,13 +26,22 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { constructor(IEntryPoint _entryPoint) EIP712(name, version) KernelStorage(_entryPoint) {} - /// @notice initialize wallet kernel - /// @dev this function should be called only once, implementation initialize is blocked by owner = address(1) - /// @param _owner owner address - function initialize(address _owner) external { - WalletKernelStorage storage ws = getKernelStorage(); - require(ws.owner == address(0), "account: already initialized"); - ws.owner = _owner; + fallback() external payable { + // should we do entrypoint check here? + bytes4 sig = msg.sig; + address facet = getKernelStorage().facets[sig]; + assembly { + calldatacopy(0,0,calldatasize()) + let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0) + returndatacopy(0, 0, returndatasize()) + switch result + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } } /// @notice Query plugin for data @@ -86,41 +95,14 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { returns (uint256 validationData) { require(msg.sender == address(entryPoint), "account: not from entryPoint"); - if (userOp.signature.length == 65) { + address plugin = getKernelStorage().plugins[bytes4(userOp.callData[0:4])]; + if( plugin == address(0) && getKernelStorage().defaultPlugin != address(0)) { + plugin = getKernelStorage().defaultPlugin; + } + if (plugin == address(0)) { validationData = _validateUserOp(userOp, userOpHash); - } else if (userOp.signature.length > 97) { - // userOp.signature = address(plugin) + validUntil + validAfter + pluginData + pluginSignature - address plugin = address(bytes20(userOp.signature[0:20])); - uint48 validUntil = uint48(bytes6(userOp.signature[20:26])); - uint48 validAfter = uint48(bytes6(userOp.signature[26:32])); - bytes memory signature = userOp.signature[32:97]; - (bytes memory data,) = abi.decode(userOp.signature[97:], (bytes, bytes)); - bytes32 digest = _hashTypedDataV4( - keccak256( - abi.encode( - keccak256( - "ValidateUserOpPlugin(address plugin,uint48 validUntil,uint48 validAfter,bytes data)" - ), // we are going to trust plugin for verification - plugin, - validUntil, - validAfter, - keccak256(data) - ) - ) - ); - - address signer = ECDSA.recover(digest, signature); - if (getKernelStorage().owner != signer) { - return SIG_VALIDATION_FAILED; - } - bytes memory ret = _delegateToPlugin(plugin, userOp, userOpHash, missingAccountFunds); - bool res = abi.decode(ret, (bool)); - if (!res) { - return SIG_VALIDATION_FAILED; - } - validationData = _packValidationData(!res, validUntil, validAfter); } else { - revert InvalidSignatureLength(); + validationData = _delegateToPlugin(plugin, userOp, userOpHash, missingAccountFunds); } if (missingAccountFunds > 0) { // we are going to assume signature is valid at this point @@ -147,24 +129,8 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { } } - /** - * delegate the contract call to the plugin - */ - function _delegateToPlugin( - address plugin, - UserOperation calldata userOp, - bytes32 opHash, - uint256 missingAccountFunds - ) internal returns (bytes memory) { - bytes memory data = - abi.encodeWithSelector(IPlugin.validatePluginData.selector, userOp, opHash, missingAccountFunds); - (bool success, bytes memory ret) = Exec.delegateCall(plugin, data); // Q: should we allow value > 0? - if (!success) { - assembly { - revert(add(ret, 32), mload(ret)) - } - } - return ret; + function _delegateToPlugin(address _plugin, UserOperation calldata _userOp, bytes32 _userOpHash, uint256 _missingFunds) internal returns(uint256 ret){ + // } /// @notice validate signature using eip1271 @@ -185,4 +151,4 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { return 0xffffffff; } } -} +} \ No newline at end of file diff --git a/src/abstract/KernelStorage.sol b/src/abstract/KernelStorage.sol index ae4d3bb0..0c02fa58 100644 --- a/src/abstract/KernelStorage.sol +++ b/src/abstract/KernelStorage.sol @@ -5,6 +5,9 @@ import "account-abstraction/interfaces/IEntryPoint.sol"; struct WalletKernelStorage { address owner; + address defaultPlugin; + mapping(bytes4 => address) plugins; + mapping(bytes4 => address) facets; } contract KernelStorage { @@ -28,6 +31,16 @@ contract KernelStorage { entryPoint = _entryPoint; getKernelStorage().owner = address(1); } + + /// @notice initialize wallet kernel + /// @dev this function should be called only once, implementation initialize is blocked by owner = address(1) + /// @param _owner owner address + function initialize(address _owner) external { + WalletKernelStorage storage ws = getKernelStorage(); + require(ws.owner == address(0), "account: already initialized"); + ws.owner = _owner; + } + /// @notice get wallet kernel storage /// @dev used to get wallet kernel storage /// @return ws wallet kernel storage, consists of owner and nonces @@ -62,5 +75,17 @@ contract KernelStorage { function getNonce(uint192 key) public view virtual returns (uint256) { return entryPoint.getNonce(address(this), key); } + + function setPlugin(bytes4 _selector, address _plugin) external onlyFromEntryPointOrOwnerOrSelf { + getKernelStorage().plugins[_selector] = _plugin; + } + + function setDefaultPlugin(address _plugin) external onlyFromEntryPointOrOwnerOrSelf { + getKernelStorage().defaultPlugin = _plugin; + } + + function addFacet(bytes4 _selector, address _facet) external onlyFromEntryPointOrOwnerOrSelf { + getKernelStorage().facets[_selector] = _facet; + } } \ No newline at end of file From ff5a176d3dd7db369c611fd1327aa2591535bdab Mon Sep 17 00:00:00 2001 From: leekt Date: Thu, 27 Apr 2023 22:54:00 +0900 Subject: [PATCH 02/18] kernel draft for mode based wallet --- src/Kernel.sol | 84 ++++++++++--- src/KernelFactory.sol | 6 +- src/abstract/KernelStorage.sol | 1 - src/plugin/IPlugin.sol | 8 +- src/plugin/ZeroDevBasePlugin.sol | 42 ------- src/plugin/ZeroDevSessionKeyPlugin.sol | 93 -------------- src/test/TestCounter.sol | 4 +- test/Kernel.t.sol | 105 ---------------- test/hardhat/sessionkey.test.ts | 162 ------------------------- 9 files changed, 76 insertions(+), 429 deletions(-) delete mode 100644 src/plugin/ZeroDevBasePlugin.sol delete mode 100644 src/plugin/ZeroDevSessionKeyPlugin.sol delete mode 100644 test/Kernel.t.sol delete mode 100644 test/hardhat/sessionkey.test.ts diff --git a/src/Kernel.sol b/src/Kernel.sol index 3652455a..9d63f4db 100644 --- a/src/Kernel.sol +++ b/src/Kernel.sol @@ -6,7 +6,7 @@ import "./plugin/IPlugin.sol"; import "account-abstraction/core/Helpers.sol"; import "account-abstraction/interfaces/IAccount.sol"; import "account-abstraction/interfaces/IEntryPoint.sol"; -import {EntryPoint} from "account-abstraction/core/EntryPoint.sol"; +import {EntryPoint} from "account-abstraction/core/EntryPoint.sol"; import "./utils/Exec.sol"; import "./abstract/Compatibility.sol"; import "./abstract/KernelStorage.sol"; @@ -31,16 +31,12 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { bytes4 sig = msg.sig; address facet = getKernelStorage().facets[sig]; assembly { - calldatacopy(0,0,calldatasize()) + calldatacopy(0, 0, calldatasize()) let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) switch result - case 0 { - revert(0, returndatasize()) - } - default { - return(0, returndatasize()) - } + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } } } @@ -65,7 +61,7 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { /// @param value value to be sent /// @param data data to be sent /// @param operation operation type (call or delegatecall) - function executeAndRevert(address to, uint256 value, bytes calldata data, Operation operation) external { + function execute(address to, uint256 value, bytes calldata data, Operation operation) external { require( msg.sender == address(entryPoint) || msg.sender == getKernelStorage().owner, "account: not from entrypoint or owner" @@ -94,16 +90,42 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { external returns (uint256 validationData) { + // validateUserOp has to be able to handle + // 1) 2fa + // 2) session key + // signature + // 4 bytes + // <--------> | <-----------> | <----------> + // mode(0) data_plugin sig_plugin => plugin mode, use delegatecall + // <--------> | <---------------> | <-----------> | <----------> | <------------> + // mode(1) addr_plugin_hot data_plugin sig_plugin sig_verifier => plugin override mode, use call(to be safe) + // + // interface IPlugin { + // function pluginValidation( + // userOp, + // opHash, + // pluginDataAndSig + // ) external returns(uint256 validationData, bytes calldata data); + // } + // require(msg.sender == address(entryPoint), "account: not from entryPoint"); - address plugin = getKernelStorage().plugins[bytes4(userOp.callData[0:4])]; - if( plugin == address(0) && getKernelStorage().defaultPlugin != address(0)) { + bytes4 sig = bytes4(userOp.callData[0:4]); + address plugin = getKernelStorage().plugins[sig]; + if(plugin == address(0)) { plugin = getKernelStorage().defaultPlugin; } - if (plugin == address(0)) { - validationData = _validateUserOp(userOp, userOpHash); + // mode based signature + bytes4 mode = bytes4(userOp.signature[0:4]); // mode == 00..00 use plugins + // validation phase + if(mode == 0x00000001) { + plugin = address(bytes20(userOp.signature[4: 24])); + _hotPluginValidation(plugin, userOp, userOpHash, userOp.signature[24:]); + } else if ( plugin == address(0)) { + validationData = _validateUserOp(userOp.signature[4:], userOpHash); } else { - validationData = _delegateToPlugin(plugin, userOp, userOpHash, missingAccountFunds); + (validationData,) = _delegateToPlugin(plugin, userOp, userOpHash, userOp.signature[4:]); } + if (missingAccountFunds > 0) { // we are going to assume signature is valid at this point (bool success,) = msg.sender.call{value: missingAccountFunds}(""); @@ -112,25 +134,47 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { } } - function _validateUserOp(UserOperation calldata userOp, bytes32 userOpHash) + function _validateUserOp(bytes calldata signature, bytes32 userOpHash) internal view returns (uint256 validationData) { WalletKernelStorage storage ws = getKernelStorage(); - if (ws.owner == ECDSA.recover(userOpHash, userOp.signature)) { + if (ws.owner == ECDSA.recover(userOpHash, signature)) { return validationData; } bytes32 hash = ECDSA.toEthSignedMessageHash(userOpHash); - address recovered = ECDSA.recover(hash, userOp.signature); + address recovered = ECDSA.recover(hash, signature); if (ws.owner != recovered) { return SIG_VALIDATION_FAILED; } } - function _delegateToPlugin(address _plugin, UserOperation calldata _userOp, bytes32 _userOpHash, uint256 _missingFunds) internal returns(uint256 ret){ - // + function _delegateToPlugin(address plugin, UserOperation calldata userOp, bytes32 opHash, bytes calldata pluginDataAndSig) + internal + returns (uint256, bytes32) + { + bytes memory data = abi.encodeWithSelector(IPlugin.validatePluginData.selector, userOp, opHash, pluginDataAndSig); + (bool success, bytes memory ret) = Exec.delegateCall(plugin, data); // Q: should we allow value > 0? + if (!success || ret.length != 32) { + // return 0 (SIG_VALIDATION_FAILED) + return (0, bytes32(0)); + } + return abi.decode(ret, (uint256, bytes32)); + } + + function _hotPluginValidation(address plugin, UserOperation calldata userOp, bytes32 opHash, bytes calldata pluginDataAndSig) + internal + returns (uint256, bytes32) + { + bytes memory data = abi.encodeWithSelector(IPlugin.validatePluginData.selector, userOp, opHash, pluginDataAndSig); + (bool success, bytes memory ret) = Exec.call(plugin,0, data); + if (!success || ret.length != 32) { + // return 0 (SIG_VALIDATION_FAILED) + return (0, bytes32(0)); + } + return abi.decode(ret, (uint256, bytes32)); } /// @notice validate signature using eip1271 @@ -151,4 +195,4 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { return 0xffffffff; } } -} \ No newline at end of file +} diff --git a/src/KernelFactory.sol b/src/KernelFactory.sol index b74d3247..4cf6c309 100644 --- a/src/KernelFactory.sol +++ b/src/KernelFactory.sol @@ -21,7 +21,7 @@ contract KernelFactory { keccak256( abi.encodePacked( type(EIP1967Proxy).creationCode, - abi.encode(address(kernelTemplate), abi.encodeCall(Kernel.initialize, (_owner))) + abi.encode(address(kernelTemplate), abi.encodeCall(KernelStorage.initialize, (_owner))) ) ) ); @@ -29,7 +29,7 @@ contract KernelFactory { return EIP1967Proxy(payable(addr)); } proxy = - new EIP1967Proxy{salt: salt}(address(kernelTemplate), abi.encodeWithSelector(Kernel.initialize.selector, _owner)); + new EIP1967Proxy{salt: salt}(address(kernelTemplate), abi.encodeWithSelector(KernelStorage.initialize.selector, _owner)); emit AccountCreated(address(proxy), _owner, _index); } @@ -40,7 +40,7 @@ contract KernelFactory { keccak256( abi.encodePacked( type(EIP1967Proxy).creationCode, - abi.encode(address(kernelTemplate), abi.encodeCall(Kernel.initialize, (_owner))) + abi.encode(address(kernelTemplate), abi.encodeCall(KernelStorage.initialize, (_owner))) ) ) ); diff --git a/src/abstract/KernelStorage.sol b/src/abstract/KernelStorage.sol index 0c02fa58..2b0a3692 100644 --- a/src/abstract/KernelStorage.sol +++ b/src/abstract/KernelStorage.sol @@ -88,4 +88,3 @@ contract KernelStorage { getKernelStorage().facets[_selector] = _facet; } } - \ No newline at end of file diff --git a/src/plugin/IPlugin.sol b/src/plugin/IPlugin.sol index 7fdf5e4d..0d213d66 100644 --- a/src/plugin/IPlugin.sol +++ b/src/plugin/IPlugin.sol @@ -4,7 +4,11 @@ pragma solidity ^0.8.0; import "account-abstraction/interfaces/UserOperation.sol"; interface IPlugin { - function validatePluginData(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) + function validatePluginData( + UserOperation calldata userOp, + bytes32 userOpHash, + bytes calldata pluginDataAndSig + ) external - returns (bool); + returns (uint256 validationData, bytes32 dataHash); } diff --git a/src/plugin/ZeroDevBasePlugin.sol b/src/plugin/ZeroDevBasePlugin.sol deleted file mode 100644 index 4cf5c8f7..00000000 --- a/src/plugin/ZeroDevBasePlugin.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; -import "openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; -import "account-abstraction/interfaces/IAccount.sol"; -import "account-abstraction/interfaces/IEntryPoint.sol"; -import "./IPlugin.sol"; -abstract contract ZeroDevBasePlugin is IPlugin, EIP712 { - function validatePluginData(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) - external - override - returns (bool validated) - { - // data offset starts at 97 - (bytes calldata data, bytes calldata signature) = parseDataAndSignature(userOp.signature[97:]); - validated = _validatePluginData(userOp, userOpHash, data, signature); - } - - function _validatePluginData( - UserOperation calldata userOp, - bytes32 userOpHash, - bytes calldata data, - bytes calldata signature - ) internal virtual returns (bool success); - - function parseDataAndSignature(bytes calldata _packed) - public - pure - returns (bytes calldata data, bytes calldata signature) - { - uint256 dataPosition = uint256(bytes32(_packed[0:32])); - uint256 dataLength = uint256(bytes32(_packed[dataPosition:dataPosition + 32])); - uint256 signaturePosition = uint256(bytes32(_packed[32:64])); - uint256 signatureLength = uint256(bytes32(_packed[signaturePosition:signaturePosition + 32])); - data = _packed[dataPosition + 32:dataPosition + 32 + dataLength]; - signature = _packed[signaturePosition + 32:signaturePosition + 32 + signatureLength]; - - require(dataPosition + 64 + ((dataLength) / 32) * 32 == signaturePosition, "invalid data"); - require(signaturePosition + 64 + ((signatureLength) / 32) * 32 == _packed.length, "invalid signature"); - } -} diff --git a/src/plugin/ZeroDevSessionKeyPlugin.sol b/src/plugin/ZeroDevSessionKeyPlugin.sol deleted file mode 100644 index 572862a3..00000000 --- a/src/plugin/ZeroDevSessionKeyPlugin.sol +++ /dev/null @@ -1,93 +0,0 @@ -//SPDX-License-Identifier: GPL -pragma solidity ^0.8.7; - -/* solhint-disable avoid-low-level-calls */ -/* solhint-disable no-inline-assembly */ -/* solhint-disable reason-string */ - -import "./ZeroDevBasePlugin.sol"; -import "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol"; -using ECDSA for bytes32; -/** - * Main EIP4337 module. - * Called (through the fallback module) using "delegate" from the GnosisSafe as an "IAccount", - * so must implement validateUserOp - * holds an immutable reference to the EntryPoint - */ - -struct ZeroDevSessionKeyStorageStruct { - mapping(address => bool) revoked; -} - -contract ZeroDevSessionKeyPlugin is ZeroDevBasePlugin { - // return value in case of signature failure, with no time-range. - // equivalent to packSigTimeRange(true,0,0); - uint256 internal constant SIG_VALIDATION_FAILED = 1; - - event SessionKeyRevoked(address indexed key); - - constructor() EIP712("ZeroDevSessionKeyPlugin", "0.0.1") {} - - function getPolicyStorage() internal pure returns (ZeroDevSessionKeyStorageStruct storage s) { - bytes32 position = bytes32(uint256(keccak256("zero-dev.account.eip4337.sessionkey")) - 1); - assembly { - s.slot := position - } - } - - // revoke session key - function revokeSessionKey(address _key) external { - getPolicyStorage().revoked[_key] = true; - emit SessionKeyRevoked(_key); - } - - function revoked(address _key) external view returns (bool) { - return getPolicyStorage().revoked[_key]; - } - - function _validatePluginData( - UserOperation calldata userOp, - bytes32 userOpHash, - bytes calldata data, - bytes calldata signature - ) internal view override returns (bool) { - address sessionKey = address(bytes20(data[0:20])); - require(!getPolicyStorage().revoked[sessionKey], "session key revoked"); - bytes32 merkleRoot = bytes32(data[20:52]); - if(merkleRoot == bytes32(0)) { - // means this session key has sudo permission - signature = signature[33:98]; - } else { - uint8 leafLength = uint8(signature[0]); - bytes32[] memory proof; - bytes32 leaf; - if(leafLength == 20) { - leaf = keccak256(signature[1:21]); - proof = abi.decode(signature[86:], (bytes32[])); - require(keccak256(userOp.callData[16:36]) == keccak256(signature[1:21]), "invalid session key"); - signature = signature[21:86]; - } else if(leafLength == 24) { - leaf = keccak256(signature[1:25]); - proof = abi.decode(signature[90:], (bytes32[])); - require(keccak256(userOp.callData[16:36]) == keccak256(signature[1:21]), "invalid session key"); - uint256 offset = uint256(bytes32(userOp.callData[68:100])); - bytes calldata sig = userOp.callData[offset + 36: offset + 40]; - require(keccak256(sig) == keccak256(signature[21:25])); - signature = signature[25:90]; - } - require(MerkleProof.verify(proof, merkleRoot, leaf), "invalide merkle root"); - } - bytes32 digest = _hashTypedDataV4( - keccak256( - abi.encode( - keccak256("Session(bytes32 userOpHash,uint256 nonce)"), // we are going to trust plugin for verification - userOpHash, - userOp.nonce - ) - ) - ); - address recovered = digest.recover(signature); - require(recovered == sessionKey, "account: invalid signature"); - return true; - } -} diff --git a/src/test/TestCounter.sol b/src/test/TestCounter.sol index 1d74483c..4b211061 100644 --- a/src/test/TestCounter.sol +++ b/src/test/TestCounter.sol @@ -1,9 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; + contract TestCounter { uint256 public counter; + function increment() public { counter += 1; } -} \ No newline at end of file +} diff --git a/test/Kernel.t.sol b/test/Kernel.t.sol deleted file mode 100644 index bef5f99b..00000000 --- a/test/Kernel.t.sol +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "forge-std/Test.sol"; - -import "account-abstraction/core/EntryPoint.sol"; -import "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; - -import {Kernel, KernelStorage} from "src/Kernel.sol"; -import {KernelFactory} from "src/KernelFactory.sol"; -import {SimpleAccountFactory, SimpleAccount} from "account-abstraction/samples/SimpleAccountFactory.sol"; -import {TestCounter} from "account-abstraction/test/TestCounter.sol"; - -using ECDSA for bytes32; - -contract KernelTest is Test { - EntryPoint entryPoint; - KernelFactory accountFactory; - Kernel kernelTemplate; - TestCounter testCounter; - - address payable bundler; - address user1; - uint256 user1PrivKey; - - function setUp() public { - entryPoint = new EntryPoint(); - accountFactory = new KernelFactory(entryPoint); - (user1, user1PrivKey) = makeAddrAndKey("user1"); - kernelTemplate = new Kernel(entryPoint); - bundler = payable(makeAddr("bundler")); - testCounter = new TestCounter(); - } - - function signUserOp(UserOperation memory op, address addr, uint256 key) - public - view - returns (bytes memory signature) - { - bytes32 hash = entryPoint.getUserOpHash(op); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, hash.toEthSignedMessageHash()); - require(addr == ECDSA.recover(hash.toEthSignedMessageHash(), v, r, s)); - signature = abi.encodePacked(r, s, v); - require(addr == ECDSA.recover(hash.toEthSignedMessageHash(), signature)); - } - - function testDeploySampleAccount() public { - SimpleAccountFactory simpleAccountFactory = new SimpleAccountFactory(entryPoint); - address payable account = payable(simpleAccountFactory.getAddress(user1, 0)); - entryPoint.depositTo{value: 1000000000000000000}(account); - UserOperation[] memory ops = new UserOperation[](1); - ops[0] = UserOperation({ - sender: account, - nonce: 0, - initCode: abi.encodePacked(simpleAccountFactory, abi.encodeCall(SimpleAccountFactory.createAccount, (user1, 0))), - callData: abi.encodeCall( - SimpleAccount.execute, (address(testCounter), 0, abi.encodeCall(TestCounter.count, ())) - ), - callGasLimit: 100000, - verificationGasLimit: 200000, - preVerificationGas: 200000, - maxFeePerGas: 100000, - maxPriorityFeePerGas: 100000, - paymasterAndData: hex"", - signature: hex"" - }); - ops[0].signature = signUserOp(ops[0], user1, user1PrivKey); - entryPoint.handleOps(ops, bundler); - ops[0] = UserOperation({ - sender: account, - nonce: 1, - initCode: hex"", - callData: abi.encodeCall( - SimpleAccount.execute, (address(testCounter), 0, abi.encodeCall(TestCounter.count, ())) - ), - callGasLimit: 100000, - verificationGasLimit: 200000, - preVerificationGas: 200000, - maxFeePerGas: 100000, - maxPriorityFeePerGas: 100000, - paymasterAndData: hex"", - signature: hex"" - }); - ops[0].signature = signUserOp(ops[0], user1, user1PrivKey); - entryPoint.handleOps(ops, bundler); - ops[0] = UserOperation({ - sender: account, - nonce: 2, - initCode: hex"", - callData: abi.encodeCall( - SimpleAccount.execute, (address(testCounter), 0, abi.encodeCall(TestCounter.count, ())) - ), - callGasLimit: 100000, - verificationGasLimit: 200000, - preVerificationGas: 200000, - maxFeePerGas: 100000, - maxPriorityFeePerGas: 100000, - paymasterAndData: hex"", - signature: hex"" - }); - ops[0].signature = signUserOp(ops[0], user1, user1PrivKey); - entryPoint.handleOps(ops, bundler); - } -} diff --git a/test/hardhat/sessionkey.test.ts b/test/hardhat/sessionkey.test.ts deleted file mode 100644 index 84818380..00000000 --- a/test/hardhat/sessionkey.test.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { MerkleTree } from 'merkletreejs'; -import keccak256 from 'keccak256'; -import { expect } from 'chai'; -import { ethers } from 'hardhat' -import { Signer, BytesLike, BigNumber } from 'ethers'; -import { hexConcat, hexZeroPad } from 'ethers/lib/utils'; -import { AccountFactory, AccountFactory__factory, Kernel, Kernel__factory, TestCounter, TestCounter__factory, ZeroDevSessionKeyPlugin, ZeroDevSessionKeyPlugin__factory } from '../../typechain-types'; -import { EntryPoint, EntryPoint__factory } from "../../types"; - -async function signSessionKey(kernel: Kernel, sessionPlugin: ZeroDevSessionKeyPlugin, owner: Signer, sessionKey: Signer, merkleRoot: string): Promise { - const ownerSig = await owner._signTypedData( - { - name: "Kernel", - version: "0.0.1", - chainId: await ethers.provider.getNetwork().then(x => x.chainId), - verifyingContract: kernel.address, - }, - { - ValidateUserOpPlugin: [ - { name: "plugin", type: "address" }, - { name: "validUntil", type: "uint48" }, - { name: "validAfter", type: "uint48" }, - { name: "data", type: "bytes" }, - ] - }, - { - plugin: sessionPlugin.address, - validUntil: 0, - validAfter: 0, - data: hexConcat([ - await sessionKey.getAddress(), - hexZeroPad(merkleRoot, 32), - ]) - } - ); - return ownerSig; -} - -async function getSessionSig(nonce: BigNumber, kernel: Kernel, sessionKey: Signer, userOpHash: BytesLike): Promise { - const sessionsig = await sessionKey._signTypedData( - { - name: "ZeroDevSessionKeyPlugin", - version: "0.0.1", - chainId: await ethers.provider.getNetwork().then(x => x.chainId), - verifyingContract: kernel.address, - }, - { - Session: [ - { name: "userOpHash", type: "bytes32" }, - { name: "nonce", type: "uint256" }, - ] - }, - { - userOpHash: hexZeroPad(userOpHash, 32), - nonce: nonce - } - ); - return sessionsig; -} - -describe('SessionKey', function () { - let sessionKey: ZeroDevSessionKeyPlugin; - let owner: Signer; - let entrypoint: EntryPoint; - let accountFactory: AccountFactory; - let kernelTemplate: Kernel; - let kernel: Kernel; - let testCounter: TestCounter; - let session: Signer; - let merkle: MerkleTree; - beforeEach(async function () { - [owner, session] = await ethers.getSigners(); - entrypoint = await new EntryPoint__factory(owner).deploy(); - sessionKey = await new ZeroDevSessionKeyPlugin__factory(owner).deploy(); - accountFactory = await new AccountFactory__factory(owner).deploy(entrypoint.address); - kernelTemplate = await new Kernel__factory(owner).deploy(entrypoint.address); - await accountFactory.createAccount(await owner.getAddress(), 0); - kernel = Kernel__factory.connect(await accountFactory.getAccountAddress(await owner.getAddress(), 0), owner); - await kernel.upgradeTo(kernelTemplate.address); - testCounter = await new TestCounter__factory(owner).deploy(); - }) - it("test", async function () { - const nonce = await entrypoint.getNonce(kernel.address, await session.getAddress()); - let op = { - sender: kernel.address, - nonce: nonce, - initCode: "0x", - callData: kernel.interface.encodeFunctionData("executeAndRevert", [ - testCounter.address, - 0, - testCounter.interface.encodeFunctionData("increment"), - 0 - ]), - callGasLimit: 100000, - verificationGasLimit: 200000, - preVerificationGas: 100000, - maxFeePerGas: 100000, - maxPriorityFeePerGas: 100000, - paymasterAndData: "0x", - signature: "0x" - } - const userOpHash = (await entrypoint.getUserOpHash(op)); - - merkle = new MerkleTree( - [ - hexZeroPad(testCounter.address, 20) - ], - keccak256, - { sortPairs: true, hashLeaves: true } - ); - console.log("hexZeroPad :", hexZeroPad(testCounter.address, 20)); - console.log("length :", hexZeroPad(testCounter.address, 20).length); - const proof = merkle.getHexProof(ethers.utils.keccak256(testCounter.address)); - console.log("testCounter :", testCounter.address); - console.log("merkle root :", merkle.getRoot().toString('hex')); - console.log(proof); - const ownerSig = await signSessionKey( - kernel, - sessionKey, - owner, - session, - "0x" + merkle.getRoot().toString('hex') - ); - - await owner.sendTransaction({ - to: kernel.address, - value: ethers.utils.parseEther("10.0") - }); - - const sessionsig = await getSessionSig(nonce, kernel, session, userOpHash); - console.log("owner address : ", await owner.getAddress()); - console.log("owner balance before : ", await owner.getBalance()); - op.signature = hexConcat([ - hexConcat([ - sessionKey.address, - hexZeroPad("0x00", 12), // validUntil + validAfter - ownerSig, // signature - ]), - ethers.utils.defaultAbiCoder.encode([ - "bytes", - "bytes" - ], [ - hexConcat([ - await session.getAddress(), - hexZeroPad("0x" + merkle.getRoot().toString('hex'), 32), - ]), - hexConcat([ - hexZeroPad("0x14", 1), - testCounter.address, - hexZeroPad(sessionsig, 65), - ethers.utils.defaultAbiCoder.encode([ - "bytes32[]" - ], [ - proof - ]), - ]) - ])]), - - await entrypoint.handleOps([op], await owner.getAddress()) - console.log("owner balance after : ", await owner.getBalance()); - }) -}) From cacb5e6b6124d88ac5cee87bfcd756b68d971b58 Mon Sep 17 00:00:00 2001 From: leekt Date: Thu, 11 May 2023 03:11:41 +0900 Subject: [PATCH 03/18] compilable --- README.md | 24 +++-- foundry.toml | 1 + src/IKernel.sol | 6 ++ src/Kernel.sol | 171 ++++++++++++------------------- src/KernelFactory.sol | 48 --------- src/abstract/Compatibility.sol | 2 - src/abstract/KernelStorage.sol | 40 +++----- src/actions/ERC721Actions.sol | 10 ++ src/plugin/IPlugin.sol | 14 --- src/validator/ECDSAValidator.sol | 46 +++++++++ src/validator/IValidator.sol | 18 ++++ 11 files changed, 177 insertions(+), 203 deletions(-) create mode 100644 src/IKernel.sol delete mode 100644 src/KernelFactory.sol create mode 100644 src/actions/ERC721Actions.sol delete mode 100644 src/plugin/IPlugin.sol create mode 100644 src/validator/ECDSAValidator.sol create mode 100644 src/validator/IValidator.sol diff --git a/README.md b/README.md index fee8b4ac..27b911e6 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,21 @@ # Kernel -A minimal and efficient ERC-4337-compatible smart contract account designed to be extended. +## Modular smart contract -## Build +Adding new feature will be same as adding a new facet for erc2535 diamond standard. -Make sure [Foundry](https://github.com/foundry-rs/foundry) is installed. Then: +For example, if you want to add a erc721 transfer feature, you can add a new facet for erc721 transfer feature. -``` -forge install -forge build -forge test -``` +And all those features has it's own validation logic, which has to be done through `validateUserOp` function + +this validation logic can be set by the user, and it can be changed by user + +So essentially, there will be +1. validation module per function +2. diamond facet for implementing the function + +## Things to consider for implementing the validation module + +In Kernel, validation module is called with `call` not `delegatecall`, which means that the validation module can not change the state of the Kernel itself. + +But, this does comes with some limitation, **STORAGE ACCESS RULE**. Since erc4337 does not allow the userOp validation to access any storage outside of the account except the storage slot is related to the account address. So, if you are developing the Kernel validation module, you have to set the storage to not access any storage that violates the rule. \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 21caefb3..3c1604e9 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,5 +3,6 @@ src = 'src' out = 'out' libs = ['lib'] remappings = ['account-abstraction/=lib/account-abstraction/contracts/'] +via_ir = true # See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/src/IKernel.sol b/src/IKernel.sol new file mode 100644 index 00000000..a92885f9 --- /dev/null +++ b/src/IKernel.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +interface IKernel { +} diff --git a/src/Kernel.sol b/src/Kernel.sol index 9d63f4db..6bcefe37 100644 --- a/src/Kernel.sol +++ b/src/Kernel.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; import "openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; -import "./plugin/IPlugin.sol"; import "account-abstraction/core/Helpers.sol"; import "account-abstraction/interfaces/IAccount.sol"; import "account-abstraction/interfaces/IEntryPoint.sol"; @@ -14,7 +13,7 @@ import "./abstract/KernelStorage.sol"; /// @title Kernel /// @author taek /// @notice wallet kernel for minimal wallet functionality -/// @dev supports only 1 owner, multiple plugins +/// @dev supports only 1 owner, multiple validators contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { error InvalidNonce(); error InvalidSignatureLength(); @@ -22,14 +21,15 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { string public constant name = "Kernel"; - string public constant version = "0.0.1"; + string public constant version = "0.0.2"; constructor(IEntryPoint _entryPoint) EIP712(name, version) KernelStorage(_entryPoint) {} fallback() external payable { // should we do entrypoint check here? + require(msg.sender == address(entryPoint), "account: not from entrypoint"); bytes4 sig = msg.sig; - address facet = getKernelStorage().facets[sig]; + address facet = getKernelStorage().action[sig]; assembly { calldatacopy(0, 0, calldatasize()) let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0) @@ -40,21 +40,6 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { } } - /// @notice Query plugin for data - /// @dev this function will always fail, it should be used only to query plugin for data using error message - /// @param _plugin Plugin address - /// @param _data Data to query - function queryPlugin(address _plugin, bytes calldata _data) external { - (bool success, bytes memory _ret) = Exec.delegateCall(_plugin, _data); - if (success) { - revert QueryResult(_ret); - } else { - assembly { - revert(add(_ret, 32), mload(_ret)) - } - } - } - /// @notice execute function call to external contract /// @dev this function will execute function call to external contract /// @param to target contract address @@ -63,8 +48,8 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { /// @param operation operation type (call or delegatecall) function execute(address to, uint256 value, bytes calldata data, Operation operation) external { require( - msg.sender == address(entryPoint) || msg.sender == getKernelStorage().owner, - "account: not from entrypoint or owner" + msg.sender == address(entryPoint), + "account: not from entrypoint" ); bool success; bytes memory ret; @@ -90,42 +75,59 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { external returns (uint256 validationData) { - // validateUserOp has to be able to handle - // 1) 2fa - // 2) session key - // signature - // 4 bytes - // <--------> | <-----------> | <----------> - // mode(0) data_plugin sig_plugin => plugin mode, use delegatecall - // <--------> | <---------------> | <-----------> | <----------> | <------------> - // mode(1) addr_plugin_hot data_plugin sig_plugin sig_verifier => plugin override mode, use call(to be safe) - // - // interface IPlugin { - // function pluginValidation( - // userOp, - // opHash, - // pluginDataAndSig - // ) external returns(uint256 validationData, bytes calldata data); - // } - // require(msg.sender == address(entryPoint), "account: not from entryPoint"); bytes4 sig = bytes4(userOp.callData[0:4]); - address plugin = getKernelStorage().plugins[sig]; - if(plugin == address(0)) { - plugin = getKernelStorage().defaultPlugin; + IKernelValidator validator = getKernelStorage().validator[sig]; + if(address(validator) == address(0)) { + validator = getKernelStorage().defaultValidator; } // mode based signature - bytes4 mode = bytes4(userOp.signature[0:4]); // mode == 00..00 use plugins - // validation phase - if(mode == 0x00000001) { - plugin = address(bytes20(userOp.signature[4: 24])); - _hotPluginValidation(plugin, userOp, userOpHash, userOp.signature[24:]); - } else if ( plugin == address(0)) { - validationData = _validateUserOp(userOp.signature[4:], userOpHash); + bytes4 mode = bytes4(userOp.signature[0:4]); // mode == 00..00 use validators + + if(mode == 0x00000000) { + // use validators + // validate signature + UserOperation memory op = userOp; + op.signature = userOp.signature[4:]; + validator.validateUserOp(op, userOpHash, missingAccountFunds); + } else if(mode == 0x00000001) { + // enable validator + IKernelValidator newValidator = IKernelValidator(address(bytes20(userOp.signature[4:20]))); + uint48 validUntil = uint48(bytes6(userOp.signature[20:26])); + uint48 validAfter = uint48(bytes6(userOp.signature[26:32])); + uint256 enableDataLength = uint256(bytes32(userOp.signature[32:64])); + bytes calldata enableData = userOp.signature[64:64+enableDataLength]; + uint256 enableSignatureLength = uint256(bytes32(userOp.signature[64+enableDataLength:96+enableDataLength])); + bytes calldata enableSignature = userOp.signature[96+enableDataLength:96+enableDataLength+enableSignatureLength]; + bytes32 enableDigest = _hashTypedDataV4(keccak256(abi.encode( + keccak256("EnableValidator(bytes4 sig,address newValidator,uint48 validUntil,uint48 validAfter,bytes enableData)"), + sig, + newValidator, + validUntil, + validAfter, + keccak256(enableData) + ))); + validationData = getKernelStorage().defaultValidator.validateSignature( + enableDigest, + enableSignature + ); + ValidationData memory data = _parseValidationData(validationData); + if(data.aggregator != address(0)) { + return SIG_VALIDATION_FAILED; + } + validator.enable(enableData); + // // validate signature + UserOperation memory op = userOp; + op.signature = userOp.signature[68+enableDataLength+enableSignatureLength:]; + validationData = validator.validateUserOp(op, userOpHash, missingAccountFunds); + } else if(mode == 0x00000002) { + // sudo mode (use default validator) + UserOperation memory op = userOp; + op.signature = userOp.signature[4:]; + validationData = getKernelStorage().defaultValidator.validateUserOp(op, userOpHash, missingAccountFunds); } else { - (validationData,) = _delegateToPlugin(plugin, userOp, userOpHash, userOp.signature[4:]); + return SIG_VALIDATION_FAILED; } - if (missingAccountFunds > 0) { // we are going to assume signature is valid at this point (bool success,) = msg.sender.call{value: missingAccountFunds}(""); @@ -134,65 +136,20 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { } } - function _validateUserOp(bytes calldata signature, bytes32 userOpHash) - internal - view - returns (uint256 validationData) - { - WalletKernelStorage storage ws = getKernelStorage(); - if (ws.owner == ECDSA.recover(userOpHash, signature)) { - return validationData; - } - - bytes32 hash = ECDSA.toEthSignedMessageHash(userOpHash); - address recovered = ECDSA.recover(hash, signature); - if (ws.owner != recovered) { - return SIG_VALIDATION_FAILED; - } - } - - function _delegateToPlugin(address plugin, UserOperation calldata userOp, bytes32 opHash, bytes calldata pluginDataAndSig) - internal - returns (uint256, bytes32) - { - bytes memory data = abi.encodeWithSelector(IPlugin.validatePluginData.selector, userOp, opHash, pluginDataAndSig); - (bool success, bytes memory ret) = Exec.delegateCall(plugin, data); // Q: should we allow value > 0? - if (!success || ret.length != 32) { - // return 0 (SIG_VALIDATION_FAILED) - return (0, bytes32(0)); - } - return abi.decode(ret, (uint256, bytes32)); - } - - function _hotPluginValidation(address plugin, UserOperation calldata userOp, bytes32 opHash, bytes calldata pluginDataAndSig) - internal - returns (uint256, bytes32) - { - bytes memory data = abi.encodeWithSelector(IPlugin.validatePluginData.selector, userOp, opHash, pluginDataAndSig); - (bool success, bytes memory ret) = Exec.call(plugin,0, data); - if (!success || ret.length != 32) { - // return 0 (SIG_VALIDATION_FAILED) - return (0, bytes32(0)); + // TODO: this should be forwarded to default validator + function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { + uint256 validationData = getKernelStorage().defaultValidator.validateSignature(hash, signature); + ValidationData memory data = _parseValidationData(validationData); + if(data.validAfter > block.timestamp) { + return 0xffffffff; } - return abi.decode(ret, (uint256, bytes32)); - } - - /// @notice validate signature using eip1271 - /// @dev this function will validate signature using eip1271 - /// @param _hash hash to be signed - /// @param _signature signature - function isValidSignature(bytes32 _hash, bytes memory _signature) public view override returns (bytes4) { - WalletKernelStorage storage ws = getKernelStorage(); - if (ws.owner == ECDSA.recover(_hash, _signature)) { - return 0x1626ba7e; + if(data.validUntil < block.timestamp) { + return 0xffffffff; } - bytes32 hash = ECDSA.toEthSignedMessageHash(_hash); - address recovered = ECDSA.recover(hash, _signature); - // Validate signatures - if (ws.owner == recovered) { - return 0x1626ba7e; - } else { + if(data.aggregator != address(0)) { return 0xffffffff; } + + return 0x1626ba7e; } } diff --git a/src/KernelFactory.sol b/src/KernelFactory.sol deleted file mode 100644 index 4cf6c309..00000000 --- a/src/KernelFactory.sol +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "openzeppelin-contracts/contracts/utils/Create2.sol"; -import "./factory/EIP1967Proxy.sol"; -import "./Kernel.sol"; - -contract KernelFactory { - Kernel public immutable kernelTemplate; - - event AccountCreated(address indexed account, address indexed owner, uint256 index); - - constructor(IEntryPoint _entryPoint) { - kernelTemplate = new Kernel(_entryPoint); - } - - function createAccount(address _owner, uint256 _index) external returns (EIP1967Proxy proxy) { - bytes32 salt = keccak256(abi.encodePacked(_owner, _index)); - address addr = Create2.computeAddress( - salt, - keccak256( - abi.encodePacked( - type(EIP1967Proxy).creationCode, - abi.encode(address(kernelTemplate), abi.encodeCall(KernelStorage.initialize, (_owner))) - ) - ) - ); - if (addr.code.length > 0) { - return EIP1967Proxy(payable(addr)); - } - proxy = - new EIP1967Proxy{salt: salt}(address(kernelTemplate), abi.encodeWithSelector(KernelStorage.initialize.selector, _owner)); - emit AccountCreated(address(proxy), _owner, _index); - } - - function getAccountAddress(address _owner, uint256 _index) public view returns (address) { - bytes32 salt = keccak256(abi.encodePacked(_owner, _index)); - return Create2.computeAddress( - salt, - keccak256( - abi.encodePacked( - type(EIP1967Proxy).creationCode, - abi.encode(address(kernelTemplate), abi.encodeCall(KernelStorage.initialize, (_owner))) - ) - ) - ); - } -} diff --git a/src/abstract/Compatibility.sol b/src/abstract/Compatibility.sol index 97861276..98a71a80 100644 --- a/src/abstract/Compatibility.sol +++ b/src/abstract/Compatibility.sol @@ -19,6 +19,4 @@ abstract contract Compatibility { { return this.onERC1155BatchReceived.selector; } - - function isValidSignature(bytes32 _hash, bytes memory _signature) public view virtual returns (bytes4); } diff --git a/src/abstract/KernelStorage.sol b/src/abstract/KernelStorage.sol index 2b0a3692..1baa1833 100644 --- a/src/abstract/KernelStorage.sol +++ b/src/abstract/KernelStorage.sol @@ -2,12 +2,12 @@ pragma solidity ^0.8.0; import "account-abstraction/interfaces/IEntryPoint.sol"; +import "src/validator/IValidator.sol"; struct WalletKernelStorage { - address owner; - address defaultPlugin; - mapping(bytes4 => address) plugins; - mapping(bytes4 => address) facets; + IKernelValidator defaultValidator; + mapping(bytes4 => IKernelValidator) validator; + mapping(bytes4 => address) action; } contract KernelStorage { @@ -21,7 +21,7 @@ contract KernelStorage { // the account itself modifier onlyFromEntryPointOrOwnerOrSelf() { require( - msg.sender == address(entryPoint) || msg.sender == getKernelStorage().owner || msg.sender == address(this), + msg.sender == address(entryPoint) || msg.sender == address(this), "account: not from entrypoint or owner or self" ); _; @@ -29,16 +29,16 @@ contract KernelStorage { constructor(IEntryPoint _entryPoint) { entryPoint = _entryPoint; - getKernelStorage().owner = address(1); + getKernelStorage().defaultValidator = IKernelValidator(address(1)); } /// @notice initialize wallet kernel /// @dev this function should be called only once, implementation initialize is blocked by owner = address(1) - /// @param _owner owner address - function initialize(address _owner) external { + /// @param _defaultValidator owner address + function initialize(IKernelValidator _defaultValidator) external { WalletKernelStorage storage ws = getKernelStorage(); - require(ws.owner == address(0), "account: already initialized"); - ws.owner = _owner; + require(address(ws.defaultValidator) == address(0), "account: already initialized"); + ws.defaultValidator = _defaultValidator; } /// @notice get wallet kernel storage @@ -52,10 +52,6 @@ contract KernelStorage { } } - function getOwner() external view returns (address) { - return getKernelStorage().owner; - } - function upgradeTo(address _newImplementation) external onlyFromEntryPointOrOwnerOrSelf { bytes32 slot = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; assembly { @@ -64,10 +60,6 @@ contract KernelStorage { emit Upgraded(_newImplementation); } - function transferOwnership(address _newOwner) external onlyFromEntryPointOrOwnerOrSelf { - getKernelStorage().owner = _newOwner; - } - function getNonce() public view virtual returns (uint256) { return entryPoint.getNonce(address(this), 0); } @@ -76,15 +68,15 @@ contract KernelStorage { return entryPoint.getNonce(address(this), key); } - function setPlugin(bytes4 _selector, address _plugin) external onlyFromEntryPointOrOwnerOrSelf { - getKernelStorage().plugins[_selector] = _plugin; + function setValidator(bytes4 _selector, IKernelValidator _validator) external onlyFromEntryPointOrOwnerOrSelf { + getKernelStorage().validator[_selector] = _validator; } - function setDefaultPlugin(address _plugin) external onlyFromEntryPointOrOwnerOrSelf { - getKernelStorage().defaultPlugin = _plugin; + function setDefaultValidator(IKernelValidator _defaultValidator) external onlyFromEntryPointOrOwnerOrSelf { + getKernelStorage().defaultValidator = _defaultValidator; } - function addFacet(bytes4 _selector, address _facet) external onlyFromEntryPointOrOwnerOrSelf { - getKernelStorage().facets[_selector] = _facet; + function addAction(bytes4 _selector, address _action) external onlyFromEntryPointOrOwnerOrSelf { + getKernelStorage().action[_selector] = _action; } } diff --git a/src/actions/ERC721Actions.sol b/src/actions/ERC721Actions.sol new file mode 100644 index 00000000..e45cbf70 --- /dev/null +++ b/src/actions/ERC721Actions.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "openzeppelin-contracts/contracts/token/ERC721/IERC721.sol"; + +contract ERC721Actions { + function transferERC721Action(address _token, uint256 _id, address _to) external { + IERC721(_token).transferFrom(address(this), _to, _id); + } +} \ No newline at end of file diff --git a/src/plugin/IPlugin.sol b/src/plugin/IPlugin.sol deleted file mode 100644 index 0d213d66..00000000 --- a/src/plugin/IPlugin.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "account-abstraction/interfaces/UserOperation.sol"; - -interface IPlugin { - function validatePluginData( - UserOperation calldata userOp, - bytes32 userOpHash, - bytes calldata pluginDataAndSig - ) - external - returns (uint256 validationData, bytes32 dataHash); -} diff --git a/src/validator/ECDSAValidator.sol b/src/validator/ECDSAValidator.sol new file mode 100644 index 00000000..780ed633 --- /dev/null +++ b/src/validator/ECDSAValidator.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./IValidator.sol"; +import "openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; + +struct ECDSAValidatorStorage { + address owner; +} + +uint256 constant SIG_VALIDATION_FAILED = 1; + +contract ECDSAValidator is IKernelValidator { + event OwnerChanged(address indexed oldOwner, address indexed newOwner); + mapping(address => ECDSAValidatorStorage) public ecdsaValidatorStorage; + + function disable(bytes calldata) external pure override { + revert("not supported"); + } + + function enable(bytes calldata _data) external override { + address owner = address(bytes20(_data[0:20])); + address oldOwner = ecdsaValidatorStorage[owner].owner; + ecdsaValidatorStorage[owner].owner = owner; + emit OwnerChanged(oldOwner, owner); + } + + function validateUserOp(UserOperation calldata _userOp, bytes32 _userOpHash, uint256) external view override returns(uint256 validationData) { + address owner = ecdsaValidatorStorage[_userOp.sender].owner; + if (owner == ECDSA.recover(_userOpHash, _userOp.signature)) { + return 0; + } + + bytes32 hash = ECDSA.toEthSignedMessageHash(_userOpHash); + address recovered = ECDSA.recover(hash, _userOp.signature); + if (owner != recovered) { + return SIG_VALIDATION_FAILED; + } + } + + function validateSignature(bytes32 hash, bytes calldata signature) public view override returns (uint256) { + address owner = ecdsaValidatorStorage[msg.sender].owner; + return owner == ECDSA.recover(hash, signature) ? 0: 1; + } +} \ No newline at end of file diff --git a/src/validator/IValidator.sol b/src/validator/IValidator.sol new file mode 100644 index 00000000..76334cb8 --- /dev/null +++ b/src/validator/IValidator.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "account-abstraction/interfaces/UserOperation.sol"; +interface IKernelValidator { + function enable(bytes calldata _data) external; + + function disable(bytes calldata _data) external; + + function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingFunds) external returns(uint256); + + function validateSignature(bytes32 hash, bytes calldata signature) external view returns (uint256); +} + +// 3 modes +// 1. default mode, use preset validator for the kernel +// 2. enable mode, enable a new validator for given action and use it for current userOp +// 3. sudo mode, use default plugin for current userOp \ No newline at end of file From c5c5e1b5e93196a6dc2234af825aa49ccb3d84b8 Mon Sep 17 00:00:00 2001 From: leekt Date: Thu, 11 May 2023 04:04:07 +0900 Subject: [PATCH 04/18] merged 2 function into 1 --- src/Kernel.sol | 5 ++--- src/abstract/KernelStorage.sol | 17 ++++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Kernel.sol b/src/Kernel.sol index 6bcefe37..ee4fa60b 100644 --- a/src/Kernel.sol +++ b/src/Kernel.sol @@ -29,7 +29,7 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { // should we do entrypoint check here? require(msg.sender == address(entryPoint), "account: not from entrypoint"); bytes4 sig = msg.sig; - address facet = getKernelStorage().action[sig]; + address facet = getKernelStorage().execution[sig].executor; assembly { calldatacopy(0, 0, calldatasize()) let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0) @@ -77,7 +77,7 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { { require(msg.sender == address(entryPoint), "account: not from entryPoint"); bytes4 sig = bytes4(userOp.callData[0:4]); - IKernelValidator validator = getKernelStorage().validator[sig]; + IKernelValidator validator = getKernelStorage().execution[sig].validator; if(address(validator) == address(0)) { validator = getKernelStorage().defaultValidator; } @@ -136,7 +136,6 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { } } - // TODO: this should be forwarded to default validator function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { uint256 validationData = getKernelStorage().defaultValidator.validateSignature(hash, signature); ValidationData memory data = _parseValidationData(validationData); diff --git a/src/abstract/KernelStorage.sol b/src/abstract/KernelStorage.sol index 1baa1833..c7398de9 100644 --- a/src/abstract/KernelStorage.sol +++ b/src/abstract/KernelStorage.sol @@ -4,10 +4,15 @@ pragma solidity ^0.8.0; import "account-abstraction/interfaces/IEntryPoint.sol"; import "src/validator/IValidator.sol"; +struct ExectionDetail { + address executor; + IKernelValidator validator; +} + struct WalletKernelStorage { + address __deprecated; IKernelValidator defaultValidator; - mapping(bytes4 => IKernelValidator) validator; - mapping(bytes4 => address) action; + mapping(bytes4 => ExectionDetail) execution; } contract KernelStorage { @@ -68,15 +73,13 @@ contract KernelStorage { return entryPoint.getNonce(address(this), key); } - function setValidator(bytes4 _selector, IKernelValidator _validator) external onlyFromEntryPointOrOwnerOrSelf { - getKernelStorage().validator[_selector] = _validator; + function setExecution(bytes4 _selector, address _executor, IKernelValidator _validator) external onlyFromEntryPointOrOwnerOrSelf { + getKernelStorage().execution[_selector].executor = _executor; + getKernelStorage().execution[_selector].validator = _validator; } function setDefaultValidator(IKernelValidator _defaultValidator) external onlyFromEntryPointOrOwnerOrSelf { getKernelStorage().defaultValidator = _defaultValidator; } - function addAction(bytes4 _selector, address _action) external onlyFromEntryPointOrOwnerOrSelf { - getKernelStorage().action[_selector] = _action; - } } From 69cabb83ab48932bd73899feee5fa7a0f24f0f1b Mon Sep 17 00:00:00 2001 From: leekt Date: Mon, 15 May 2023 00:58:15 +0900 Subject: [PATCH 05/18] minimal function for disabling the non-sudo mode --- lcov.info | 554 +++++++++++++------------------ src/IKernel.sol | 3 +- src/Kernel.sol | 118 ++++--- src/abstract/KernelStorage.sol | 33 +- src/actions/ERC721Actions.sol | 2 +- src/test/TestValidator.sol | 18 + src/utils/KernelHelper.sol | 14 + src/validator/ECDSAValidator.sol | 21 +- src/validator/IValidator.sol | 9 +- test/foundry/ERC4337Utils.sol | 33 ++ test/foundry/Kernel.test.sol | 117 +++++++ 11 files changed, 518 insertions(+), 404 deletions(-) create mode 100644 src/test/TestValidator.sol create mode 100644 src/utils/KernelHelper.sol create mode 100644 test/foundry/ERC4337Utils.sol create mode 100644 test/foundry/Kernel.test.sol diff --git a/lcov.info b/lcov.info index b5a7364a..d961ed67 100644 --- a/lcov.info +++ b/lcov.info @@ -1,42 +1,45 @@ TN: SF:src/Kernel.sol -FN:34,Kernel.initialize -FNDA:0,Kernel.initialize -DA:35,0 -DA:36,0 -BRDA:36,0,0,- -BRDA:36,0,1,- -DA:37,0 -FN:44,Kernel.queryPlugin -FNDA:0,Kernel.queryPlugin -DA:45,0 -DA:46,0 -BRDA:46,1,0,- -BRDA:46,1,1,- -DA:47,0 -FN:61,Kernel.executeAndRevert -FNDA:0,Kernel.executeAndRevert -DA:67,0 -BRDA:67,2,0,- -BRDA:67,2,1,- -DA:68,0 -DA:69,0 -DA:70,0 -BRDA:70,3,0,- -BRDA:70,3,1,- -DA:71,0 -DA:73,0 -DA:75,0 -BRDA:75,4,0,- -BRDA:75,4,1,- -FN:88,Kernel.validateUserOp -FNDA:0,Kernel.validateUserOp +FN:28,Kernel. +FNDA:0,Kernel. +DA:29,0 +BRDA:29,0,0,- +BRDA:29,0,1,- +DA:30,0 +DA:31,0 +FN:48,Kernel.execute +FNDA:0,Kernel.execute +DA:49,0 +BRDA:49,1,0,- +BRDA:49,1,1,- +DA:50,0 +DA:51,0 +DA:52,0 +BRDA:52,2,0,- +BRDA:52,2,1,- +DA:53,0 +DA:55,0 +DA:57,0 +BRDA:57,3,0,- +BRDA:57,3,1,- +FN:70,Kernel.validateUserOp +FNDA:3,Kernel.validateUserOp +DA:74,3 +BRDA:74,4,0,- +BRDA:74,4,1,3 +DA:76,3 +DA:77,3 +BRDA:77,5,0,- +BRDA:77,5,1,3 +DA:79,0 +DA:84,3 +BRDA:84,6,0,- +BRDA:84,6,1,3 +DA:86,3 +DA:87,3 +DA:88,3 DA:90,0 -BRDA:90,5,0,- -BRDA:90,5,1,- DA:91,0 -BRDA:91,6,0,- -BRDA:91,6,1,- DA:92,0 BRDA:92,7,0,- BRDA:92,7,1,- @@ -44,82 +47,68 @@ DA:93,0 DA:94,0 BRDA:94,8,0,- BRDA:94,8,1,- -DA:96,0 +DA:95,0 DA:97,0 -DA:98,0 -DA:99,0 -DA:100,0 +BRDA:97,9,0,- +BRDA:97,9,1,- DA:102,0 -DA:111,0 +DA:103,0 +DA:104,0 +DA:105,0 +BRDA:105,10,0,- +BRDA:105,10,1,- +DA:106,0 +DA:108,0 DA:112,0 -BRDA:112,9,0,- -BRDA:112,9,1,- -DA:113,0 -DA:115,0 -DA:121,0 -DA:122,0 -BRDA:122,10,0,- -BRDA:122,10,1,- -DA:123,0 -DA:125,0 -DA:127,0 +DA:115,3 +BRDA:115,11,0,- +BRDA:115,11,1,3 +DA:117,3 +DA:119,3 +FN:123,Kernel._approveValidator +FNDA:0,Kernel._approveValidator +DA:128,0 DA:129,0 -BRDA:129,11,0,- -BRDA:129,11,1,- DA:130,0 -DA:132,0 -FN:136,Kernel._validateUserOp -FNDA:0,Kernel._validateUserOp -DA:138,0 -DA:139,0 -DA:140,0 +DA:131,0 DA:141,0 -BRDA:141,12,0,- -BRDA:141,12,1,- -DA:142,0 -DA:145,0 -BRDA:145,13,0,- -BRDA:145,13,1,- -DA:146,0 -BRDA:146,14,0,- -BRDA:146,14,1,- DA:147,0 -FN:155,Kernel._delegateToPlugin -FNDA:0,Kernel._delegateToPlugin +DA:148,0 +FN:151,Kernel.isValidSignature +FNDA:1,Kernel.isValidSignature +DA:152,1 +DA:153,1 +DA:154,1 +BRDA:154,12,0,- +BRDA:154,12,1,1 +DA:155,0 +DA:157,1 +BRDA:157,13,0,- +BRDA:157,13,1,1 +DA:158,0 +DA:160,1 +BRDA:160,14,0,- +BRDA:160,14,1,1 DA:161,0 -DA:166,0 -DA:167,0 -BRDA:167,15,0,- -BRDA:167,15,1,- -DA:172,0 -FN:179,Kernel.isValidSignature -FNDA:0,Kernel.isValidSignature -DA:183,0 -DA:184,0 -DA:185,0 -DA:187,0 -BRDA:187,16,0,- -BRDA:187,16,1,- -DA:188,0 -DA:190,0 -FNF:7 -FNH:0 -LF:54 -LH:0 -BRF:34 -BRH:0 +DA:164,1 +FNF:5 +FNH:2 +LF:51 +LH:16 +BRF:30 +BRH:7 end_of_record TN: SF:src/abstract/Compatibility.sol -FN:8,Compatibility.onERC721Received +FN:7,Compatibility.onERC721Received FNDA:0,Compatibility.onERC721Received -DA:14,0 -FN:17,Compatibility.onERC1155Received +DA:8,0 +FN:11,Compatibility.onERC1155Received FNDA:0,Compatibility.onERC1155Received -DA:24,0 -FN:27,Compatibility.onERC1155BatchReceived +DA:12,0 +FN:15,Compatibility.onERC1155BatchReceived FNDA:0,Compatibility.onERC1155BatchReceived -DA:34,0 +DA:20,0 FNF:3 FNH:0 LF:3 @@ -129,49 +118,74 @@ BRH:0 end_of_record TN: SF:src/abstract/KernelStorage.sol -FN:13,KernelStorage.getKernelStorage -FNDA:768,KernelStorage.getKernelStorage -DA:14,768 -DA:16,768 +FN:44,KernelStorage.initialize +FNDA:2,KernelStorage.initialize +DA:45,2 +DA:46,2 +BRDA:46,0,0,1 +BRDA:46,0,1,1 +DA:47,1 +DA:48,1 +FN:54,KernelStorage.getKernelStorage +FNDA:15,KernelStorage.getKernelStorage +DA:55,15 +DA:57,15 +FN:61,KernelStorage.upgradeTo +FNDA:0,KernelStorage.upgradeTo +DA:62,0 +DA:66,0 +FN:70,KernelStorage.getNonce +FNDA:0,KernelStorage.getNonce +DA:71,0 +FN:74,KernelStorage.getNonce +FNDA:0,KernelStorage.getNonce +DA:75,0 +FN:79,KernelStorage.getDefaultValidator +FNDA:1,KernelStorage.getDefaultValidator +DA:80,1 +FN:83,KernelStorage.getDisabledMode +FNDA:1,KernelStorage.getDisabledMode +DA:84,1 +FN:87,KernelStorage.getExecution +FNDA:1,KernelStorage.getExecution +DA:88,1 +FN:92,KernelStorage.setExecution +FNDA:1,KernelStorage.setExecution +DA:96,1 +FN:99,KernelStorage.setDefaultValidator +FNDA:1,KernelStorage.setDefaultValidator +DA:100,1 +FN:103,KernelStorage.disableMode +FNDA:1,KernelStorage.disableMode +DA:104,1 +FNF:11 +FNH:8 +LF:16 +LH:12 +BRF:2 +BRH:2 +end_of_record +TN: +SF:src/actions/ERC721Actions.sol +FN:7,ERC721Actions.transferERC721Action +FNDA:0,ERC721Actions.transferERC721Action +DA:8,0 FNF:1 -FNH:1 -LF:2 -LH:2 +FNH:0 +LF:1 +LH:0 BRF:0 BRH:0 end_of_record TN: -SF:src/factory/AccountFactory.sol -FN:15,AccountFactory.createAccount -FNDA:256,AccountFactory.createAccount -DA:16,256 -DA:17,256 -DA:24,256 -BRDA:24,0,0,- -BRDA:24,0,1,256 -DA:25,0 -DA:27,256 -DA:28,256 -FN:31,AccountFactory.getAccountAddress -FNDA:256,AccountFactory.getAccountAddress -DA:32,256 -DA:33,256 -FNF:2 -FNH:2 -LF:8 -LH:7 -BRF:2 -BRH:1 -end_of_record -TN: SF:src/factory/EIP1967Proxy.sol FN:24,EIP1967Proxy. -FNDA:256,EIP1967Proxy. -DA:25,256 -FN:50,EIP1967Proxy._implementation -FNDA:256,EIP1967Proxy._implementation -DA:51,256 -DA:53,256 +FNDA:11,EIP1967Proxy. +DA:25,11 +FN:46,EIP1967Proxy._implementation +FNDA:11,EIP1967Proxy._implementation +DA:47,11 +DA:49,11 FNF:2 FNH:2 LF:3 @@ -180,209 +194,50 @@ BRF:0 BRH:0 end_of_record TN: -SF:src/factory/MinimalAccount.sol -FN:25,MinimalAccount.initialize -FNDA:256,MinimalAccount.initialize -DA:26,256 -BRDA:26,0,0,- -BRDA:26,0,1,256 -DA:27,256 -FN:30,MinimalAccount.getOwner -FNDA:256,MinimalAccount.getOwner -DA:31,256 -FN:34,MinimalAccount.getNonce -FNDA:0,MinimalAccount.getNonce -DA:35,0 -FN:38,MinimalAccount.validateUserOp -FNDA:0,MinimalAccount.validateUserOp -DA:39,0 -BRDA:39,1,0,- -BRDA:39,1,1,- -DA:40,0 -BRDA:40,2,0,- -BRDA:40,2,1,- -DA:41,0 -DA:42,0 -DA:43,0 -DA:44,0 -BRDA:44,3,0,- -BRDA:44,3,1,- -DA:45,0 -DA:48,0 -BRDA:48,4,0,- -BRDA:48,4,1,- -DA:49,0 -BRDA:49,5,0,- -BRDA:49,5,1,- -DA:50,0 -DA:54,0 -BRDA:54,6,0,- -BRDA:54,6,1,- -DA:55,0 -DA:58,0 -FN:67,MinimalAccount.executeAndRevert -FNDA:0,MinimalAccount.executeAndRevert -DA:73,0 -BRDA:73,7,0,- -BRDA:73,7,1,- -DA:74,0 -DA:75,0 -DA:76,0 -BRDA:76,8,0,- -BRDA:76,8,1,- -DA:77,0 -DA:79,0 -DA:81,0 -BRDA:81,9,0,- -BRDA:81,9,1,- -FNF:5 -FNH:2 -LF:24 -LH:3 -BRF:20 -BRH:1 -end_of_record -TN: -SF:src/plugin/ZeroDevBasePlugin.sol -FN:10,ZeroDevBasePlugin.validatePluginData -FNDA:0,ZeroDevBasePlugin.validatePluginData -DA:16,0 -DA:17,0 -DA:18,0 -BRDA:18,0,0,- -BRDA:18,0,1,- -DA:20,0 -FN:33,ZeroDevBasePlugin.parseDataAndSignature -FNDA:0,ZeroDevBasePlugin.parseDataAndSignature -DA:36,0 -DA:37,0 -DA:38,0 -DA:39,0 -DA:40,0 -DA:41,0 -DA:43,0 -BRDA:43,1,0,- -BRDA:43,1,1,- -DA:44,0 -BRDA:44,2,0,- -BRDA:44,2,1,- -FNF:2 -FNH:0 -LF:12 -LH:0 -BRF:6 -BRH:0 -end_of_record -TN: -SF:src/plugin/ZeroDevSessionKeyPlugin.sol -FN:35,ZeroDevSessionKeyPlugin.getPolicyStorage -FNDA:0,ZeroDevSessionKeyPlugin.getPolicyStorage -DA:36,0 -DA:38,0 -FN:43,ZeroDevSessionKeyPlugin.revokeSessionKey -FNDA:0,ZeroDevSessionKeyPlugin.revokeSessionKey -DA:44,0 -DA:45,0 -FN:48,ZeroDevSessionKeyPlugin.revoked -FNDA:0,ZeroDevSessionKeyPlugin.revoked -DA:49,0 -FN:52,ZeroDevSessionKeyPlugin.sessionNonce -FNDA:0,ZeroDevSessionKeyPlugin.sessionNonce -DA:53,0 -FN:59,ZeroDevSessionKeyPlugin._validatePluginData -FNDA:0,ZeroDevSessionKeyPlugin._validatePluginData -DA:65,0 -DA:66,0 -BRDA:66,0,0,- -BRDA:66,0,1,- -DA:67,0 -DA:70,0 -DA:71,0 -BRDA:71,1,0,- -BRDA:71,1,1,- -DA:72,0 -DA:75,0 -DA:80,0 -DA:81,0 -BRDA:81,2,0,- -BRDA:81,2,1,- -DA:82,0 -FN:85,ZeroDevSessionKeyPlugin._checkPolicy -FNDA:0,ZeroDevSessionKeyPlugin._checkPolicy -DA:86,0 -DA:87,0 -BRDA:87,3,0,- -BRDA:87,3,1,- -DA:92,0 -FNF:6 -FNH:0 -LF:19 -LH:0 -BRF:8 -BRH:0 -end_of_record -TN: -SF:src/plugin/policy/FunctionSignaturePolicy.sol -FN:22,FunctionSignaturePolicy.executeAndRevert -FNDA:0,FunctionSignaturePolicy.executeAndRevert -DA:30,0 -BRDA:29,0,0,- -BRDA:29,0,1,- -DA:32,0 -DA:34,0 -DA:35,0 +SF:src/test/TestCounter.sol +FN:8,TestCounter.increment +FNDA:0,TestCounter.increment +DA:9,0 FNF:1 FNH:0 -LF:4 -LH:0 -BRF:2 -BRH:0 -end_of_record -TN: -SF:src/plugin/policy/FunctionSignaturePolicyFactory.sol -FN:9,FunctionSignaturePolicyFactory.deploy -FNDA:0,FunctionSignaturePolicyFactory.deploy -DA:10,0 -DA:11,0 -DA:12,0 -FN:15,FunctionSignaturePolicyFactory.getPolicy -FNDA:0,FunctionSignaturePolicyFactory.getPolicy -DA:16,0 -DA:20,0 -DA:22,0 -FNF:2 -FNH:0 -LF:6 +LF:1 LH:0 BRF:0 BRH:0 end_of_record TN: -SF:src/plugin/policy/SudoPolicy.sol -FN:6,SudoPolicy.executeAndRevert -FNDA:0,SudoPolicy.executeAndRevert +SF:src/test/TestValidator.sol +FN:7,TestValidator.validateSignature +FNDA:0,TestValidator.validateSignature +DA:8,0 +FN:11,TestValidator.validateUserOp +FNDA:0,TestValidator.validateUserOp DA:12,0 -FNF:1 +FN:15,TestValidator.enable +FNDA:0,TestValidator.enable +FN:17,TestValidator.disable +FNDA:0,TestValidator.disable +FNF:4 FNH:0 -LF:1 +LF:2 LH:0 BRF:0 BRH:0 end_of_record TN: SF:src/utils/Exec.sol -FN:16,Exec.call +FN:15,Exec.call FNDA:0,Exec.call -DA:22,0 -DA:28,0 -FN:32,Exec.staticcall +DA:20,0 +DA:26,0 +FN:30,Exec.staticcall FNDA:0,Exec.staticcall -DA:37,0 -DA:43,0 -FN:48,Exec.delegateCall +DA:32,0 +DA:38,0 +FN:42,Exec.delegateCall FNDA:0,Exec.delegateCall -DA:53,0 -DA:59,0 +DA:44,0 +DA:50,0 FNF:3 FNH:0 LF:6 @@ -391,26 +246,61 @@ BRF:0 BRH:0 end_of_record TN: -SF:src/utils/ExtendedUserOpLib.sol -FN:9,ExtendedUserOpLib.checkUserOpOffset -FNDA:0,ExtendedUserOpLib.checkUserOpOffset -DA:10,0 -DA:11,0 -DA:12,0 -DA:13,0 +SF:src/validator/ECDSAValidator.sol +FN:20,ECDSAValidator.disable +FNDA:0,ECDSAValidator.disable +DA:21,0 +FN:24,ECDSAValidator.enable +FNDA:1,ECDSAValidator.enable +DA:25,1 +DA:26,1 +DA:27,1 +DA:28,1 +FN:31,ECDSAValidator.validateUserOp +FNDA:3,ECDSAValidator.validateUserOp +DA:37,3 +DA:38,3 +BRDA:38,0,0,- +BRDA:38,0,1,3 +DA:39,0 +DA:42,3 +DA:43,3 +DA:44,3 +BRDA:44,1,0,- +BRDA:44,1,1,3 +DA:45,0 +FN:49,ECDSAValidator.validateSignature +FNDA:1,ECDSAValidator.validateSignature +DA:50,1 +DA:51,1 +FNF:4 +FNH:3 +LF:14 +LH:11 +BRF:4 +BRH:2 +end_of_record +TN: +SF:test/foundry/ERC4337Utils.sol +FN:9,ERC4337Utils.fillUserOp +FNDA:0,ERC4337Utils.fillUserOp +DA:14,0 DA:15,0 -BRDA:15,0,0,- DA:16,0 +DA:17,0 DA:18,0 -BRDA:18,1,0,- DA:19,0 +DA:20,0 DA:21,0 -BRDA:21,2,0,- -DA:22,0 -FNF:1 +FN:24,ERC4337Utils.signUserOpHash +FNDA:0,ERC4337Utils.signUserOpHash +DA:29,0 +DA:30,0 +DA:31,0 +FNF:2 FNH:0 -LF:10 +LF:11 LH:0 -BRF:3 +BRF:0 BRH:0 end_of_record diff --git a/src/IKernel.sol b/src/IKernel.sol index a92885f9..0ac75871 100644 --- a/src/IKernel.sol +++ b/src/IKernel.sol @@ -2,5 +2,4 @@ pragma solidity ^0.8.0; -interface IKernel { -} +interface IKernel {} diff --git a/src/Kernel.sol b/src/Kernel.sol index ee4fa60b..4912b6e1 100644 --- a/src/Kernel.sol +++ b/src/Kernel.sol @@ -9,11 +9,11 @@ import {EntryPoint} from "account-abstraction/core/EntryPoint.sol"; import "./utils/Exec.sol"; import "./abstract/Compatibility.sol"; import "./abstract/KernelStorage.sol"; +import "./utils/KernelHelper.sol"; /// @title Kernel /// @author taek /// @notice wallet kernel for minimal wallet functionality -/// @dev supports only 1 owner, multiple validators contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { error InvalidNonce(); error InvalidSignatureLength(); @@ -26,7 +26,6 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { constructor(IEntryPoint _entryPoint) EIP712(name, version) KernelStorage(_entryPoint) {} fallback() external payable { - // should we do entrypoint check here? require(msg.sender == address(entryPoint), "account: not from entrypoint"); bytes4 sig = msg.sig; address facet = getKernelStorage().execution[sig].executor; @@ -47,10 +46,7 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { /// @param data data to be sent /// @param operation operation type (call or delegatecall) function execute(address to, uint256 value, bytes calldata data, Operation operation) external { - require( - msg.sender == address(entryPoint), - "account: not from entrypoint" - ); + require(msg.sender == address(entryPoint), "account: not from entrypoint"); bool success; bytes memory ret; if (operation == Operation.DelegateCall) { @@ -76,57 +72,45 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { returns (uint256 validationData) { require(msg.sender == address(entryPoint), "account: not from entryPoint"); - bytes4 sig = bytes4(userOp.callData[0:4]); - IKernelValidator validator = getKernelStorage().execution[sig].validator; - if(address(validator) == address(0)) { - validator = getKernelStorage().defaultValidator; - } // mode based signature bytes4 mode = bytes4(userOp.signature[0:4]); // mode == 00..00 use validators - - if(mode == 0x00000000) { - // use validators - // validate signature - UserOperation memory op = userOp; - op.signature = userOp.signature[4:]; - validator.validateUserOp(op, userOpHash, missingAccountFunds); - } else if(mode == 0x00000001) { - // enable validator - IKernelValidator newValidator = IKernelValidator(address(bytes20(userOp.signature[4:20]))); - uint48 validUntil = uint48(bytes6(userOp.signature[20:26])); - uint48 validAfter = uint48(bytes6(userOp.signature[26:32])); - uint256 enableDataLength = uint256(bytes32(userOp.signature[32:64])); - bytes calldata enableData = userOp.signature[64:64+enableDataLength]; - uint256 enableSignatureLength = uint256(bytes32(userOp.signature[64+enableDataLength:96+enableDataLength])); - bytes calldata enableSignature = userOp.signature[96+enableDataLength:96+enableDataLength+enableSignatureLength]; - bytes32 enableDigest = _hashTypedDataV4(keccak256(abi.encode( - keccak256("EnableValidator(bytes4 sig,address newValidator,uint48 validUntil,uint48 validAfter,bytes enableData)"), - sig, - newValidator, - validUntil, - validAfter, - keccak256(enableData) - ))); - validationData = getKernelStorage().defaultValidator.validateSignature( - enableDigest, - enableSignature - ); - ValidationData memory data = _parseValidationData(validationData); - if(data.aggregator != address(0)) { - return SIG_VALIDATION_FAILED; - } - validator.enable(enableData); - // // validate signature - UserOperation memory op = userOp; - op.signature = userOp.signature[68+enableDataLength+enableSignatureLength:]; - validationData = validator.validateUserOp(op, userOpHash, missingAccountFunds); - } else if(mode == 0x00000002) { + if (mode & getKernelStorage().disabledMode != 0x00000000) { + // disabled mode + return SIG_VALIDATION_FAILED; + } + // mode == 0x00000000 use sudo validator + // mode & 0x00000001 == 0x00000001 use given validator + // mode & 0x00000002 == 0x00000002 enable validator + if (mode == 0x00000000) { // sudo mode (use default validator) UserOperation memory op = userOp; op.signature = userOp.signature[4:]; validationData = getKernelStorage().defaultValidator.validateUserOp(op, userOpHash, missingAccountFunds); } else { - return SIG_VALIDATION_FAILED; + UserOperation memory op = userOp; + bytes4 sig = bytes4(userOp.callData[0:4]); + if (mode == 0x00000000) { + IKernelValidator validator = getKernelStorage().execution[sig].validator; + if (address(validator) == address(0)) { + validator = getKernelStorage().defaultValidator; + } + } else if (mode & 0x00000001 == 0x00000001) { + // use given validator + // userOp.signature[4:10] = validUntil, + // userOp.signature[10:16] = validAfter, + // userOp.signature[16:36] = validator address, + IKernelValidator validator = IKernelValidator(address(bytes20(userOp.signature[16:36]))); + bytes calldata enableData; + (validationData, enableData, op.signature) = _approveValidator(sig, userOp.signature); + if (mode & 0x00000002 == 0x00000002) { + validator.enable(enableData); + } + validationData = _intersectValidationData( + validationData, validator.validateUserOp(op, userOpHash, missingAccountFunds) + ); + } else { + return SIG_VALIDATION_FAILED; + } } if (missingAccountFunds > 0) { // we are going to assume signature is valid at this point @@ -136,16 +120,44 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { } } + function _approveValidator(bytes4 sig, bytes calldata signature) + internal + view + returns (uint256 validationData, bytes calldata enableData, bytes calldata validationSig) + { + uint256 enableDataLength = uint256(bytes32(signature[36:68])); + enableData = signature[68:68 + enableDataLength]; + uint256 enableSignatureLength = uint256(bytes32(signature[68 + enableDataLength:100 + enableDataLength])); + bytes32 enableDigest = _hashTypedDataV4( + keccak256( + abi.encode( + keccak256("ValidatorApproved(bytes4 sig,uint256 validatorData,bytes enableData)"), + bytes4(sig), + uint256(bytes32(signature[4:36])), + keccak256(enableData) + ) + ) + ); + validationData = _intersectValidationData( + getKernelStorage().defaultValidator.validateSignature( + enableDigest, signature[100 + enableDataLength:100 + enableDataLength + enableSignatureLength] + ), + uint256(bytes32(signature[4:36])) & (uint256(type(uint96).max) << 160) + ); + validationSig = signature[76 + enableDataLength + enableSignatureLength:]; + return (validationData, signature[68:68 + enableDataLength], validationSig); + } + function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { uint256 validationData = getKernelStorage().defaultValidator.validateSignature(hash, signature); ValidationData memory data = _parseValidationData(validationData); - if(data.validAfter > block.timestamp) { + if (data.validAfter > block.timestamp) { return 0xffffffff; } - if(data.validUntil < block.timestamp) { + if (data.validUntil < block.timestamp) { return 0xffffffff; } - if(data.aggregator != address(0)) { + if (data.aggregator != address(0)) { return 0xffffffff; } diff --git a/src/abstract/KernelStorage.sol b/src/abstract/KernelStorage.sol index c7398de9..ea8e1125 100644 --- a/src/abstract/KernelStorage.sol +++ b/src/abstract/KernelStorage.sol @@ -10,8 +10,9 @@ struct ExectionDetail { } struct WalletKernelStorage { - address __deprecated; + bytes32 __deprecated; IKernelValidator defaultValidator; + bytes4 disabledMode; mapping(bytes4 => ExectionDetail) execution; } @@ -40,16 +41,16 @@ contract KernelStorage { /// @notice initialize wallet kernel /// @dev this function should be called only once, implementation initialize is blocked by owner = address(1) /// @param _defaultValidator owner address - function initialize(IKernelValidator _defaultValidator) external { + function initialize(IKernelValidator _defaultValidator, bytes calldata _data) external { WalletKernelStorage storage ws = getKernelStorage(); require(address(ws.defaultValidator) == address(0), "account: already initialized"); ws.defaultValidator = _defaultValidator; + _defaultValidator.enable(_data); } /// @notice get wallet kernel storage /// @dev used to get wallet kernel storage /// @return ws wallet kernel storage, consists of owner and nonces - function getKernelStorage() internal pure returns (WalletKernelStorage storage ws) { bytes32 storagePosition = bytes32(uint256(keccak256("zerodev.kernel")) - 1); assembly { @@ -65,6 +66,7 @@ contract KernelStorage { emit Upgraded(_newImplementation); } + // nonce from entrypoint function getNonce() public view virtual returns (uint256) { return entryPoint.getNonce(address(this), 0); } @@ -73,13 +75,32 @@ contract KernelStorage { return entryPoint.getNonce(address(this), key); } - function setExecution(bytes4 _selector, address _executor, IKernelValidator _validator) external onlyFromEntryPointOrOwnerOrSelf { - getKernelStorage().execution[_selector].executor = _executor; - getKernelStorage().execution[_selector].validator = _validator; + // query storage + function getDefaultValidator() public view returns (IKernelValidator) { + return getKernelStorage().defaultValidator; + } + + function getDisabledMode() public view returns (bytes4) { + return getKernelStorage().disabledMode; + } + + function getExecution(bytes4 _selector) public view returns (ExectionDetail memory) { + return getKernelStorage().execution[_selector]; + } + + // change storage + function setExecution(bytes4 _selector, address _executor, IKernelValidator _validator) + external + onlyFromEntryPointOrOwnerOrSelf + { + getKernelStorage().execution[_selector] = ExectionDetail({executor: _executor, validator: _validator}); } function setDefaultValidator(IKernelValidator _defaultValidator) external onlyFromEntryPointOrOwnerOrSelf { getKernelStorage().defaultValidator = _defaultValidator; } + function disableMode(bytes4 _disableFlag) external onlyFromEntryPointOrOwnerOrSelf { + getKernelStorage().disabledMode = _disableFlag; + } } diff --git a/src/actions/ERC721Actions.sol b/src/actions/ERC721Actions.sol index e45cbf70..bc6aaacf 100644 --- a/src/actions/ERC721Actions.sol +++ b/src/actions/ERC721Actions.sol @@ -7,4 +7,4 @@ contract ERC721Actions { function transferERC721Action(address _token, uint256 _id, address _to) external { IERC721(_token).transferFrom(address(this), _to, _id); } -} \ No newline at end of file +} diff --git a/src/test/TestValidator.sol b/src/test/TestValidator.sol new file mode 100644 index 00000000..0d92e7aa --- /dev/null +++ b/src/test/TestValidator.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "src/validator/IValidator.sol"; + +contract TestValidator is IKernelValidator { + function validateSignature(bytes32, bytes calldata) external pure override returns (uint256) { + return 0; + } + + function validateUserOp(UserOperation calldata, bytes32, uint256) external pure override returns (uint256) { + return 0; + } + + function enable(bytes calldata) external override {} + + function disable(bytes calldata) external override {} +} diff --git a/src/utils/KernelHelper.sol b/src/utils/KernelHelper.sol new file mode 100644 index 00000000..df441972 --- /dev/null +++ b/src/utils/KernelHelper.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +function _intersectValidationData(uint256 a, uint256 b) pure returns (uint256 validationData) { + require(uint160(a) == uint160(b), "account: different aggregator"); + uint48 validAfterA = uint48(a >> 160); + uint48 validUntilA = uint48(a >> (48 + 160)); + uint48 validAfterB = uint48(b >> 160); + uint48 validUntilB = uint48(b >> (48 + 160)); + + if (validAfterA < validAfterB) validAfterA = validAfterB; + if (validUntilA > validUntilB) validUntilA = validUntilB; + validationData = uint256(uint160(a)) | (uint256(validAfterA) << 160) | (uint256(validUntilA) << (48 + 160)); +} diff --git a/src/validator/ECDSAValidator.sol b/src/validator/ECDSAValidator.sol index 780ed633..04955dba 100644 --- a/src/validator/ECDSAValidator.sol +++ b/src/validator/ECDSAValidator.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import "./IValidator.sol"; import "openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; +import "forge-std/console.sol"; struct ECDSAValidatorStorage { address owner; @@ -13,20 +14,26 @@ uint256 constant SIG_VALIDATION_FAILED = 1; contract ECDSAValidator is IKernelValidator { event OwnerChanged(address indexed oldOwner, address indexed newOwner); + mapping(address => ECDSAValidatorStorage) public ecdsaValidatorStorage; - function disable(bytes calldata) external pure override { - revert("not supported"); + function disable(bytes calldata) external override { + delete ecdsaValidatorStorage[msg.sender]; } function enable(bytes calldata _data) external override { address owner = address(bytes20(_data[0:20])); - address oldOwner = ecdsaValidatorStorage[owner].owner; - ecdsaValidatorStorage[owner].owner = owner; + address oldOwner = ecdsaValidatorStorage[msg.sender].owner; + ecdsaValidatorStorage[msg.sender].owner = owner; emit OwnerChanged(oldOwner, owner); } - function validateUserOp(UserOperation calldata _userOp, bytes32 _userOpHash, uint256) external view override returns(uint256 validationData) { + function validateUserOp(UserOperation calldata _userOp, bytes32 _userOpHash, uint256) + external + view + override + returns (uint256 validationData) + { address owner = ecdsaValidatorStorage[_userOp.sender].owner; if (owner == ECDSA.recover(_userOpHash, _userOp.signature)) { return 0; @@ -41,6 +48,6 @@ contract ECDSAValidator is IKernelValidator { function validateSignature(bytes32 hash, bytes calldata signature) public view override returns (uint256) { address owner = ecdsaValidatorStorage[msg.sender].owner; - return owner == ECDSA.recover(hash, signature) ? 0: 1; + return owner == ECDSA.recover(hash, signature) ? 0 : 1; } -} \ No newline at end of file +} diff --git a/src/validator/IValidator.sol b/src/validator/IValidator.sol index 76334cb8..5644db3b 100644 --- a/src/validator/IValidator.sol +++ b/src/validator/IValidator.sol @@ -2,17 +2,20 @@ pragma solidity ^0.8.0; import "account-abstraction/interfaces/UserOperation.sol"; + interface IKernelValidator { function enable(bytes calldata _data) external; function disable(bytes calldata _data) external; - function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingFunds) external returns(uint256); + function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingFunds) + external + returns (uint256); function validateSignature(bytes32 hash, bytes calldata signature) external view returns (uint256); } -// 3 modes +// 3 modes // 1. default mode, use preset validator for the kernel // 2. enable mode, enable a new validator for given action and use it for current userOp -// 3. sudo mode, use default plugin for current userOp \ No newline at end of file +// 3. sudo mode, use default plugin for current userOp diff --git a/test/foundry/ERC4337Utils.sol b/test/foundry/ERC4337Utils.sol new file mode 100644 index 00000000..598b466f --- /dev/null +++ b/test/foundry/ERC4337Utils.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "account-abstraction/core/EntryPoint.sol"; +import "forge-std/Test.sol"; +import "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; + +library ERC4337Utils { + function fillUserOp(EntryPoint _entryPoint, address _sender, bytes memory _data) + internal + view + returns (UserOperation memory op) + { + op.sender = _sender; + op.nonce = _entryPoint.getNonce(_sender, 0); + op.callData = _data; + op.callGasLimit = 10000000; + op.verificationGasLimit = 10000000; + op.preVerificationGas = 50000; + op.maxFeePerGas = 50000; + op.maxPriorityFeePerGas = 1; + } + + function signUserOpHash(EntryPoint _entryPoint, Vm _vm, uint256 _key, UserOperation memory _op) + internal + view + returns (bytes memory signature) + { + bytes32 hash = _entryPoint.getUserOpHash(_op); + (uint8 v, bytes32 r, bytes32 s) = _vm.sign(_key, ECDSA.toEthSignedMessageHash(hash)); + signature = abi.encodePacked(r, s, v); + } +} diff --git a/test/foundry/Kernel.test.sol b/test/foundry/Kernel.test.sol new file mode 100644 index 00000000..55821a51 --- /dev/null +++ b/test/foundry/Kernel.test.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "src/Kernel.sol"; +import "src/validator/ECDSAValidator.sol"; +import "src/factory/EIP1967Proxy.sol"; +// test artifacts +import "src/test/TestValidator.sol"; +// test utils +import "forge-std/Test.sol"; +import {ERC4337Utils} from "./ERC4337Utils.sol"; + +using ERC4337Utils for EntryPoint; + +contract KernelTest is Test { + Kernel implementation; + Kernel kernel; + EntryPoint entryPoint; + ECDSAValidator validator; + address owner; + uint256 ownerKey; + address payable beneficiary; + + function setUp() public { + (owner, ownerKey) = makeAddrAndKey("owner"); + entryPoint = new EntryPoint(); + implementation = new Kernel(entryPoint); + validator = new ECDSAValidator(); + + kernel = Kernel( + payable( + address( + new EIP1967Proxy( + address(implementation), + abi.encodeWithSelector( + implementation.initialize.selector, + validator, + abi.encodePacked(owner) + ) + ) + ) + ) + ); + vm.deal(address(kernel), 1e30); + beneficiary = payable(address(makeAddr("beneficiary"))); + } + + function test_initialize_twice() external { + vm.expectRevert(); + kernel.initialize(validator, abi.encodePacked(owner)); + } + + function test_initialize() public { + Kernel newKernel = Kernel( + payable( + address( + new EIP1967Proxy( + address(implementation), + abi.encodeWithSelector( + implementation.initialize.selector, + validator, + abi.encodePacked(owner) + ) + ) + ) + ) + ); + ECDSAValidatorStorage memory storage_ = + ECDSAValidatorStorage(validator.ecdsaValidatorStorage(address(newKernel))); + assertEq(storage_.owner, owner); + } + + function test_validate_signature() external { + bytes32 hash = keccak256(abi.encodePacked("hello world")); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey, hash); + assertEq(kernel.isValidSignature(hash, abi.encodePacked(r, s, v)), Kernel.isValidSignature.selector); + } + + function test_set_default_validator() external { + TestValidator newValidator = new TestValidator(); + UserOperation memory op = entryPoint.fillUserOp( + address(kernel), abi.encodeWithSelector(KernelStorage.setDefaultValidator.selector, address(newValidator)) + ); + op.signature = abi.encodePacked(bytes4(0x00000000), entryPoint.signUserOpHash(vm, ownerKey, op)); + UserOperation[] memory ops = new UserOperation[](1); + ops[0] = op; + entryPoint.handleOps(ops, beneficiary); + assertEq(address(KernelStorage(address(kernel)).getDefaultValidator()), address(newValidator)); + } + + function test_disable_mode() external { + UserOperation memory op = entryPoint.fillUserOp( + address(kernel), abi.encodeWithSelector(KernelStorage.disableMode.selector, bytes4(0x00000001)) + ); + op.signature = abi.encodePacked(bytes4(0x00000000), entryPoint.signUserOpHash(vm, ownerKey, op)); + UserOperation[] memory ops = new UserOperation[](1); + ops[0] = op; + entryPoint.handleOps(ops, beneficiary); + assertEq(uint256(bytes32(KernelStorage(address(kernel)).getDisabledMode())), 1 << 224); + } + + function test_set_execution() external { + UserOperation memory op = entryPoint.fillUserOp( + address(kernel), + abi.encodeWithSelector( + KernelStorage.setExecution.selector, bytes4(0xdeadbeef), address(0xdead), address(0xbeef) + ) + ); + op.signature = abi.encodePacked(bytes4(0x00000000), entryPoint.signUserOpHash(vm, ownerKey, op)); + UserOperation[] memory ops = new UserOperation[](1); + ops[0] = op; + entryPoint.handleOps(ops, beneficiary); + ExectionDetail memory execution = KernelStorage(address(kernel)).getExecution(bytes4(0xdeadbeef)); + assertEq(execution.executor, address(0xdead)); + assertEq(address(execution.validator), address(0xbeef)); + } +} From c2bd50d76e7e8c0ff80bebbb15121b59bcc62bba Mon Sep 17 00:00:00 2001 From: leekt Date: Mon, 15 May 2023 03:14:24 +0900 Subject: [PATCH 06/18] removed lcov --- lcov.info | 306 ------------------------------------------------------ 1 file changed, 306 deletions(-) delete mode 100644 lcov.info diff --git a/lcov.info b/lcov.info deleted file mode 100644 index d961ed67..00000000 --- a/lcov.info +++ /dev/null @@ -1,306 +0,0 @@ -TN: -SF:src/Kernel.sol -FN:28,Kernel. -FNDA:0,Kernel. -DA:29,0 -BRDA:29,0,0,- -BRDA:29,0,1,- -DA:30,0 -DA:31,0 -FN:48,Kernel.execute -FNDA:0,Kernel.execute -DA:49,0 -BRDA:49,1,0,- -BRDA:49,1,1,- -DA:50,0 -DA:51,0 -DA:52,0 -BRDA:52,2,0,- -BRDA:52,2,1,- -DA:53,0 -DA:55,0 -DA:57,0 -BRDA:57,3,0,- -BRDA:57,3,1,- -FN:70,Kernel.validateUserOp -FNDA:3,Kernel.validateUserOp -DA:74,3 -BRDA:74,4,0,- -BRDA:74,4,1,3 -DA:76,3 -DA:77,3 -BRDA:77,5,0,- -BRDA:77,5,1,3 -DA:79,0 -DA:84,3 -BRDA:84,6,0,- -BRDA:84,6,1,3 -DA:86,3 -DA:87,3 -DA:88,3 -DA:90,0 -DA:91,0 -DA:92,0 -BRDA:92,7,0,- -BRDA:92,7,1,- -DA:93,0 -DA:94,0 -BRDA:94,8,0,- -BRDA:94,8,1,- -DA:95,0 -DA:97,0 -BRDA:97,9,0,- -BRDA:97,9,1,- -DA:102,0 -DA:103,0 -DA:104,0 -DA:105,0 -BRDA:105,10,0,- -BRDA:105,10,1,- -DA:106,0 -DA:108,0 -DA:112,0 -DA:115,3 -BRDA:115,11,0,- -BRDA:115,11,1,3 -DA:117,3 -DA:119,3 -FN:123,Kernel._approveValidator -FNDA:0,Kernel._approveValidator -DA:128,0 -DA:129,0 -DA:130,0 -DA:131,0 -DA:141,0 -DA:147,0 -DA:148,0 -FN:151,Kernel.isValidSignature -FNDA:1,Kernel.isValidSignature -DA:152,1 -DA:153,1 -DA:154,1 -BRDA:154,12,0,- -BRDA:154,12,1,1 -DA:155,0 -DA:157,1 -BRDA:157,13,0,- -BRDA:157,13,1,1 -DA:158,0 -DA:160,1 -BRDA:160,14,0,- -BRDA:160,14,1,1 -DA:161,0 -DA:164,1 -FNF:5 -FNH:2 -LF:51 -LH:16 -BRF:30 -BRH:7 -end_of_record -TN: -SF:src/abstract/Compatibility.sol -FN:7,Compatibility.onERC721Received -FNDA:0,Compatibility.onERC721Received -DA:8,0 -FN:11,Compatibility.onERC1155Received -FNDA:0,Compatibility.onERC1155Received -DA:12,0 -FN:15,Compatibility.onERC1155BatchReceived -FNDA:0,Compatibility.onERC1155BatchReceived -DA:20,0 -FNF:3 -FNH:0 -LF:3 -LH:0 -BRF:0 -BRH:0 -end_of_record -TN: -SF:src/abstract/KernelStorage.sol -FN:44,KernelStorage.initialize -FNDA:2,KernelStorage.initialize -DA:45,2 -DA:46,2 -BRDA:46,0,0,1 -BRDA:46,0,1,1 -DA:47,1 -DA:48,1 -FN:54,KernelStorage.getKernelStorage -FNDA:15,KernelStorage.getKernelStorage -DA:55,15 -DA:57,15 -FN:61,KernelStorage.upgradeTo -FNDA:0,KernelStorage.upgradeTo -DA:62,0 -DA:66,0 -FN:70,KernelStorage.getNonce -FNDA:0,KernelStorage.getNonce -DA:71,0 -FN:74,KernelStorage.getNonce -FNDA:0,KernelStorage.getNonce -DA:75,0 -FN:79,KernelStorage.getDefaultValidator -FNDA:1,KernelStorage.getDefaultValidator -DA:80,1 -FN:83,KernelStorage.getDisabledMode -FNDA:1,KernelStorage.getDisabledMode -DA:84,1 -FN:87,KernelStorage.getExecution -FNDA:1,KernelStorage.getExecution -DA:88,1 -FN:92,KernelStorage.setExecution -FNDA:1,KernelStorage.setExecution -DA:96,1 -FN:99,KernelStorage.setDefaultValidator -FNDA:1,KernelStorage.setDefaultValidator -DA:100,1 -FN:103,KernelStorage.disableMode -FNDA:1,KernelStorage.disableMode -DA:104,1 -FNF:11 -FNH:8 -LF:16 -LH:12 -BRF:2 -BRH:2 -end_of_record -TN: -SF:src/actions/ERC721Actions.sol -FN:7,ERC721Actions.transferERC721Action -FNDA:0,ERC721Actions.transferERC721Action -DA:8,0 -FNF:1 -FNH:0 -LF:1 -LH:0 -BRF:0 -BRH:0 -end_of_record -TN: -SF:src/factory/EIP1967Proxy.sol -FN:24,EIP1967Proxy. -FNDA:11,EIP1967Proxy. -DA:25,11 -FN:46,EIP1967Proxy._implementation -FNDA:11,EIP1967Proxy._implementation -DA:47,11 -DA:49,11 -FNF:2 -FNH:2 -LF:3 -LH:3 -BRF:0 -BRH:0 -end_of_record -TN: -SF:src/test/TestCounter.sol -FN:8,TestCounter.increment -FNDA:0,TestCounter.increment -DA:9,0 -FNF:1 -FNH:0 -LF:1 -LH:0 -BRF:0 -BRH:0 -end_of_record -TN: -SF:src/test/TestValidator.sol -FN:7,TestValidator.validateSignature -FNDA:0,TestValidator.validateSignature -DA:8,0 -FN:11,TestValidator.validateUserOp -FNDA:0,TestValidator.validateUserOp -DA:12,0 -FN:15,TestValidator.enable -FNDA:0,TestValidator.enable -FN:17,TestValidator.disable -FNDA:0,TestValidator.disable -FNF:4 -FNH:0 -LF:2 -LH:0 -BRF:0 -BRH:0 -end_of_record -TN: -SF:src/utils/Exec.sol -FN:15,Exec.call -FNDA:0,Exec.call -DA:20,0 -DA:26,0 -FN:30,Exec.staticcall -FNDA:0,Exec.staticcall -DA:32,0 -DA:38,0 -FN:42,Exec.delegateCall -FNDA:0,Exec.delegateCall -DA:44,0 -DA:50,0 -FNF:3 -FNH:0 -LF:6 -LH:0 -BRF:0 -BRH:0 -end_of_record -TN: -SF:src/validator/ECDSAValidator.sol -FN:20,ECDSAValidator.disable -FNDA:0,ECDSAValidator.disable -DA:21,0 -FN:24,ECDSAValidator.enable -FNDA:1,ECDSAValidator.enable -DA:25,1 -DA:26,1 -DA:27,1 -DA:28,1 -FN:31,ECDSAValidator.validateUserOp -FNDA:3,ECDSAValidator.validateUserOp -DA:37,3 -DA:38,3 -BRDA:38,0,0,- -BRDA:38,0,1,3 -DA:39,0 -DA:42,3 -DA:43,3 -DA:44,3 -BRDA:44,1,0,- -BRDA:44,1,1,3 -DA:45,0 -FN:49,ECDSAValidator.validateSignature -FNDA:1,ECDSAValidator.validateSignature -DA:50,1 -DA:51,1 -FNF:4 -FNH:3 -LF:14 -LH:11 -BRF:4 -BRH:2 -end_of_record -TN: -SF:test/foundry/ERC4337Utils.sol -FN:9,ERC4337Utils.fillUserOp -FNDA:0,ERC4337Utils.fillUserOp -DA:14,0 -DA:15,0 -DA:16,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:21,0 -FN:24,ERC4337Utils.signUserOpHash -FNDA:0,ERC4337Utils.signUserOpHash -DA:29,0 -DA:30,0 -DA:31,0 -FNF:2 -FNH:0 -LF:11 -LH:0 -BRF:0 -BRH:0 -end_of_record From dcb9be823406a38c40e81a2acce7c1a86b54ae9a Mon Sep 17 00:00:00 2001 From: leekt Date: Mon, 15 May 2023 03:14:52 +0900 Subject: [PATCH 07/18] added more kernel permission changes --- .gitignore | 2 + src/Kernel.sol | 41 ++++---- src/abstract/KernelStorage.sol | 23 +++-- src/test/TestValidator.sol | 14 ++- src/validator/ECDSAValidator.sol | 4 +- test/foundry/Kernel.test.sol | 9 +- test/foundry/KernelExecution.test.sol | 131 ++++++++++++++++++++++++++ 7 files changed, 190 insertions(+), 34 deletions(-) create mode 100644 test/foundry/KernelExecution.test.sol diff --git a/.gitignore b/.gitignore index 5dcf5877..3978feff 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ artifacts artifacts-selected cache_hardhat + +lcov.info diff --git a/src/Kernel.sol b/src/Kernel.sol index 4912b6e1..00ffddb6 100644 --- a/src/Kernel.sol +++ b/src/Kernel.sol @@ -11,14 +11,12 @@ import "./abstract/Compatibility.sol"; import "./abstract/KernelStorage.sol"; import "./utils/KernelHelper.sol"; +import "forge-std/console.sol"; + /// @title Kernel /// @author taek /// @notice wallet kernel for minimal wallet functionality contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { - error InvalidNonce(); - error InvalidSignatureLength(); - error QueryResult(bytes result); - string public constant name = "Kernel"; string public constant version = "0.0.2"; @@ -74,10 +72,7 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { require(msg.sender == address(entryPoint), "account: not from entryPoint"); // mode based signature bytes4 mode = bytes4(userOp.signature[0:4]); // mode == 00..00 use validators - if (mode & getKernelStorage().disabledMode != 0x00000000) { - // disabled mode - return SIG_VALIDATION_FAILED; - } + require(mode & getKernelStorage().disabledMode == 0x00000000, "kernel: mode disabled"); // mode == 0x00000000 use sudo validator // mode & 0x00000001 == 0x00000001 use given validator // mode & 0x00000002 == 0x00000002 enable validator @@ -101,10 +96,9 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { // userOp.signature[16:36] = validator address, IKernelValidator validator = IKernelValidator(address(bytes20(userOp.signature[16:36]))); bytes calldata enableData; - (validationData, enableData, op.signature) = _approveValidator(sig, userOp.signature); - if (mode & 0x00000002 == 0x00000002) { - validator.enable(enableData); - } + bytes calldata remainSig; + (validationData, enableData, remainSig) = _approveValidator(sig, userOp.signature); + validator.enable(enableData); validationData = _intersectValidationData( validationData, validator.validateUserOp(op, userOpHash, missingAccountFunds) ); @@ -122,30 +116,37 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { function _approveValidator(bytes4 sig, bytes calldata signature) internal - view returns (uint256 validationData, bytes calldata enableData, bytes calldata validationSig) { - uint256 enableDataLength = uint256(bytes32(signature[36:68])); - enableData = signature[68:68 + enableDataLength]; - uint256 enableSignatureLength = uint256(bytes32(signature[68 + enableDataLength:100 + enableDataLength])); + uint256 enableDataLength = uint256(bytes32(signature[56:88])); + enableData = signature[88:88 + enableDataLength]; + uint256 enableSignatureLength = uint256(bytes32(signature[88 + enableDataLength:120 + enableDataLength])); bytes32 enableDigest = _hashTypedDataV4( keccak256( abi.encode( - keccak256("ValidatorApproved(bytes4 sig,uint256 validatorData,bytes enableData)"), + keccak256("ValidatorApproved(bytes4 sig,uint256 validatorData,address executor,bytes enableData)"), bytes4(sig), uint256(bytes32(signature[4:36])), + address(bytes20(signature[36: 56])), keccak256(enableData) ) ) ); + validationData = _intersectValidationData( getKernelStorage().defaultValidator.validateSignature( - enableDigest, signature[100 + enableDataLength:100 + enableDataLength + enableSignatureLength] + enableDigest, signature[120 + enableDataLength:120 + enableDataLength + enableSignatureLength] ), uint256(bytes32(signature[4:36])) & (uint256(type(uint96).max) << 160) ); - validationSig = signature[76 + enableDataLength + enableSignatureLength:]; - return (validationData, signature[68:68 + enableDataLength], validationSig); + validationSig = signature[120 + enableDataLength + enableSignatureLength:]; + getKernelStorage().execution[sig] = ExecutionDetail({ + executor: address(bytes20(signature[36: 56])), + validator: IKernelValidator(address(bytes20(signature[16:36]))), + validUntil: uint48(bytes6(signature[4:10])), + validAfter: uint48(bytes6(signature[10:16])) + }); + return (validationData, signature[88:88 + enableDataLength], validationSig); } function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { diff --git a/src/abstract/KernelStorage.sol b/src/abstract/KernelStorage.sol index ea8e1125..979044e1 100644 --- a/src/abstract/KernelStorage.sol +++ b/src/abstract/KernelStorage.sol @@ -4,7 +4,9 @@ pragma solidity ^0.8.0; import "account-abstraction/interfaces/IEntryPoint.sol"; import "src/validator/IValidator.sol"; -struct ExectionDetail { +struct ExecutionDetail { + uint48 validUntil; + uint48 validAfter; address executor; IKernelValidator validator; } @@ -13,7 +15,7 @@ struct WalletKernelStorage { bytes32 __deprecated; IKernelValidator defaultValidator; bytes4 disabledMode; - mapping(bytes4 => ExectionDetail) execution; + mapping(bytes4 => ExecutionDetail) execution; } contract KernelStorage { @@ -22,6 +24,8 @@ contract KernelStorage { IEntryPoint public immutable entryPoint; event Upgraded(address indexed newImplementation); + event DefaultValidatorChanged(address indexed oldValidator, address indexed newValidator); + event ExecutionChanged(bytes4 indexed selector, address indexed executor, address indexed validator); // modifier for checking if the sender is the entrypoint or // the account itself @@ -45,6 +49,7 @@ contract KernelStorage { WalletKernelStorage storage ws = getKernelStorage(); require(address(ws.defaultValidator) == address(0), "account: already initialized"); ws.defaultValidator = _defaultValidator; + emit DefaultValidatorChanged(address(0), address(_defaultValidator)); _defaultValidator.enable(_data); } @@ -84,20 +89,26 @@ contract KernelStorage { return getKernelStorage().disabledMode; } - function getExecution(bytes4 _selector) public view returns (ExectionDetail memory) { + function getExecution(bytes4 _selector) public view returns (ExecutionDetail memory) { return getKernelStorage().execution[_selector]; } // change storage - function setExecution(bytes4 _selector, address _executor, IKernelValidator _validator) + function setExecution(bytes4 _selector, address _executor, IKernelValidator _validator, uint48 _validUntil, uint48 _validAfter) external onlyFromEntryPointOrOwnerOrSelf { - getKernelStorage().execution[_selector] = ExectionDetail({executor: _executor, validator: _validator}); + getKernelStorage().execution[_selector] = ExecutionDetail({ + executor: _executor, validator: _validator, validUntil: _validUntil, validAfter: _validAfter + }); + emit ExecutionChanged(_selector, _executor, address(_validator)); } - function setDefaultValidator(IKernelValidator _defaultValidator) external onlyFromEntryPointOrOwnerOrSelf { + function setDefaultValidator(IKernelValidator _defaultValidator, bytes calldata _data) external onlyFromEntryPointOrOwnerOrSelf { + IKernelValidator oldValidator = getKernelStorage().defaultValidator; getKernelStorage().defaultValidator = _defaultValidator; + emit DefaultValidatorChanged(address(oldValidator), address(_defaultValidator)); + _defaultValidator.enable(_data); } function disableMode(bytes4 _disableFlag) external onlyFromEntryPointOrOwnerOrSelf { diff --git a/src/test/TestValidator.sol b/src/test/TestValidator.sol index 0d92e7aa..38ae6978 100644 --- a/src/test/TestValidator.sol +++ b/src/test/TestValidator.sol @@ -4,15 +4,23 @@ pragma solidity ^0.8.0; import "src/validator/IValidator.sol"; contract TestValidator is IKernelValidator { + event TestValidateUserOp(bytes32 indexed opHash); + event TestEnable(bytes data); + event TestDisable(bytes data); function validateSignature(bytes32, bytes calldata) external pure override returns (uint256) { return 0; } - function validateUserOp(UserOperation calldata, bytes32, uint256) external pure override returns (uint256) { + function validateUserOp(UserOperation calldata, bytes32 userOpHash, uint256) external override returns (uint256) { + emit TestValidateUserOp(userOpHash); return 0; } - function enable(bytes calldata) external override {} + function enable(bytes calldata data) external override { + emit TestEnable(data); + } - function disable(bytes calldata) external override {} + function disable(bytes calldata data) external override { + emit TestDisable(data); + } } diff --git a/src/validator/ECDSAValidator.sol b/src/validator/ECDSAValidator.sol index 04955dba..8be2bf50 100644 --- a/src/validator/ECDSAValidator.sol +++ b/src/validator/ECDSAValidator.sol @@ -13,7 +13,7 @@ struct ECDSAValidatorStorage { uint256 constant SIG_VALIDATION_FAILED = 1; contract ECDSAValidator is IKernelValidator { - event OwnerChanged(address indexed oldOwner, address indexed newOwner); + event OwnerChanged(address indexed kernel, address indexed oldOwner, address indexed newOwner); mapping(address => ECDSAValidatorStorage) public ecdsaValidatorStorage; @@ -25,7 +25,7 @@ contract ECDSAValidator is IKernelValidator { address owner = address(bytes20(_data[0:20])); address oldOwner = ecdsaValidatorStorage[msg.sender].owner; ecdsaValidatorStorage[msg.sender].owner = owner; - emit OwnerChanged(oldOwner, owner); + emit OwnerChanged(msg.sender, oldOwner, owner); } function validateUserOp(UserOperation calldata _userOp, bytes32 _userOpHash, uint256) diff --git a/test/foundry/Kernel.test.sol b/test/foundry/Kernel.test.sol index 55821a51..4ac96bdf 100644 --- a/test/foundry/Kernel.test.sol +++ b/test/foundry/Kernel.test.sol @@ -78,8 +78,9 @@ contract KernelTest is Test { function test_set_default_validator() external { TestValidator newValidator = new TestValidator(); + bytes memory empty; UserOperation memory op = entryPoint.fillUserOp( - address(kernel), abi.encodeWithSelector(KernelStorage.setDefaultValidator.selector, address(newValidator)) + address(kernel), abi.encodeWithSelector(KernelStorage.setDefaultValidator.selector, address(newValidator), empty) ); op.signature = abi.encodePacked(bytes4(0x00000000), entryPoint.signUserOpHash(vm, ownerKey, op)); UserOperation[] memory ops = new UserOperation[](1); @@ -103,15 +104,17 @@ contract KernelTest is Test { UserOperation memory op = entryPoint.fillUserOp( address(kernel), abi.encodeWithSelector( - KernelStorage.setExecution.selector, bytes4(0xdeadbeef), address(0xdead), address(0xbeef) + KernelStorage.setExecution.selector, bytes4(0xdeadbeef), address(0xdead), address(0xbeef), uint48(0), uint48(0) ) ); op.signature = abi.encodePacked(bytes4(0x00000000), entryPoint.signUserOpHash(vm, ownerKey, op)); UserOperation[] memory ops = new UserOperation[](1); ops[0] = op; entryPoint.handleOps(ops, beneficiary); - ExectionDetail memory execution = KernelStorage(address(kernel)).getExecution(bytes4(0xdeadbeef)); + ExecutionDetail memory execution = KernelStorage(address(kernel)).getExecution(bytes4(0xdeadbeef)); assertEq(execution.executor, address(0xdead)); assertEq(address(execution.validator), address(0xbeef)); + assertEq(uint256(execution.validUntil), uint256(0)); + assertEq(uint256(execution.validAfter), uint256(0)); } } diff --git a/test/foundry/KernelExecution.test.sol b/test/foundry/KernelExecution.test.sol new file mode 100644 index 00000000..ffdfa81d --- /dev/null +++ b/test/foundry/KernelExecution.test.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "src/Kernel.sol"; +import "src/validator/ECDSAValidator.sol"; +import "src/factory/EIP1967Proxy.sol"; +// test artifacts +import "src/test/TestValidator.sol"; +// test utils +import "forge-std/Test.sol"; +import {ERC4337Utils} from "./ERC4337Utils.sol"; + +using ERC4337Utils for EntryPoint; + +contract KernelExecutionTest is Test { + Kernel implementation; + Kernel kernel; + EntryPoint entryPoint; + ECDSAValidator validator; + address owner; + uint256 ownerKey; + address payable beneficiary; + + function setUp() public { + (owner, ownerKey) = makeAddrAndKey("owner"); + entryPoint = new EntryPoint(); + implementation = new Kernel(entryPoint); + validator = new ECDSAValidator(); + + kernel = Kernel( + payable( + address( + new EIP1967Proxy( + address(implementation), + abi.encodeWithSelector( + implementation.initialize.selector, + validator, + abi.encodePacked(owner) + ) + ) + ) + ) + ); + vm.deal(address(kernel), 1e30); + beneficiary = payable(address(makeAddr("beneficiary"))); + } + + function test_revert_when_mode_disabled() external { + UserOperation memory op = entryPoint.fillUserOp( + address(kernel), abi.encodeWithSelector(KernelStorage.disableMode.selector, bytes4(0x00000001)) + ); + op.signature = abi.encodePacked(bytes4(0x00000000), entryPoint.signUserOpHash(vm, ownerKey, op)); + UserOperation[] memory ops = new UserOperation[](1); + ops[0] = op; + entryPoint.handleOps(ops, beneficiary); + + // try to run with mode 0x00000001 + op = entryPoint.fillUserOp( + address(kernel), abi.encodeWithSelector(KernelStorage.disableMode.selector, bytes4(0x00000001)) + ); + op.signature = abi.encodePacked(bytes4(0x00000001), entryPoint.signUserOpHash(vm, ownerKey, op)); + ops[0] = op; + + vm.expectRevert(abi.encodeWithSelector(IEntryPoint.FailedOp.selector, 0, string.concat("AA23 reverted: ", "kernel: mode disabled"))); + entryPoint.handleOps(ops, beneficiary); + } + + function test_mode_1() external { + TestValidator testValidator = new TestValidator(); + UserOperation memory op = entryPoint.fillUserOp( + address(kernel), abi.encodeWithSelector(Kernel.execute.selector, address(0xdeadbeef), 1, "") + ); + + bytes32 digest = getTypedDataHash(address(kernel), Kernel.execute.selector, 0,0, address(testValidator), address(0), ""); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey, digest); + + op.signature = abi.encodePacked(bytes4(0x00000001), uint48(0), uint48(0), address(testValidator), address(0), uint256(0), uint256(65), r,s,v); + UserOperation[] memory ops = new UserOperation[](1); + ops[0] = op; + // vm.expectEmit(true, false, false, false); + // emit TestValidator.TestValidateUserOp(opHash); + entryPoint.handleOps(ops, beneficiary); + } +} + + +// computes the hash of a permit +function getStructHash(bytes4 sig, uint48 validUntil, uint48 validAfter, address validator, address executor, bytes memory enableData) + pure + returns (bytes32) +{ + return + keccak256( + abi.encode( + keccak256("ValidatorApproved(bytes4 sig,uint256 validatorData,address executor,bytes enableData)"), + bytes4(sig), + uint256(uint256(uint160(validator)) | (uint256(validAfter) << 160) | (uint256(validUntil) << (48 + 160))), + executor, + keccak256(enableData) + ) + ); +} + +// computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer +function getTypedDataHash(address sender, bytes4 sig, uint48 validUntil, uint48 validAfter, address validator, address executor, bytes memory enableData) + view + returns (bytes32) +{ + return + keccak256( + abi.encodePacked( + "\x19\x01", + _buildDomainSeparator("Kernel", "0.0.2", sender), + getStructHash(sig, validUntil, validAfter, validator, executor, enableData) + ) + ); +} + +function _buildDomainSeparator( + string memory name, + string memory version, + address verifyingContract +) view returns (bytes32) { + bytes32 hashedName = keccak256(bytes(name)); + bytes32 hashedVersion = keccak256(bytes(version)); + bytes32 typeHash = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + + return keccak256(abi.encode(typeHash, hashedName, hashedVersion, block.chainid, address(verifyingContract))); +} From 1f5f6b1897ec65c2b905ee2b7353b057893f175f Mon Sep 17 00:00:00 2001 From: leekt Date: Mon, 15 May 2023 05:40:14 +0900 Subject: [PATCH 08/18] mode 2 --- src/Kernel.sol | 23 ++++--- src/abstract/KernelStorage.sol | 21 ++++-- src/test/TestValidator.sol | 1 + test/foundry/Kernel.test.sol | 10 ++- test/foundry/KernelExecution.test.sol | 97 ++++++++++++++++----------- 5 files changed, 95 insertions(+), 57 deletions(-) diff --git a/src/Kernel.sol b/src/Kernel.sol index 00ffddb6..5c401b2e 100644 --- a/src/Kernel.sol +++ b/src/Kernel.sol @@ -84,27 +84,30 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { } else { UserOperation memory op = userOp; bytes4 sig = bytes4(userOp.callData[0:4]); - if (mode == 0x00000000) { - IKernelValidator validator = getKernelStorage().execution[sig].validator; + IKernelValidator validator; + if (mode == 0x00000001) { + ExecutionDetail storage detail = getKernelStorage().execution[sig]; + validator = detail.validator; if (address(validator) == address(0)) { validator = getKernelStorage().defaultValidator; } - } else if (mode & 0x00000001 == 0x00000001) { + op.signature = userOp.signature[4:]; + validationData = (uint256(detail.validAfter) << 160) | (uint256(detail.validUntil) << (48 + 160)); + } else if (mode & 0x00000002 == 0x00000002) { // use given validator // userOp.signature[4:10] = validUntil, // userOp.signature[10:16] = validAfter, // userOp.signature[16:36] = validator address, - IKernelValidator validator = IKernelValidator(address(bytes20(userOp.signature[16:36]))); + validator = IKernelValidator(address(bytes20(userOp.signature[16:36]))); bytes calldata enableData; bytes calldata remainSig; (validationData, enableData, remainSig) = _approveValidator(sig, userOp.signature); validator.enable(enableData); - validationData = _intersectValidationData( - validationData, validator.validateUserOp(op, userOpHash, missingAccountFunds) - ); } else { return SIG_VALIDATION_FAILED; } + validationData = + _intersectValidationData(validationData, validator.validateUserOp(op, userOpHash, missingAccountFunds)); } if (missingAccountFunds > 0) { // we are going to assume signature is valid at this point @@ -127,12 +130,12 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { keccak256("ValidatorApproved(bytes4 sig,uint256 validatorData,address executor,bytes enableData)"), bytes4(sig), uint256(bytes32(signature[4:36])), - address(bytes20(signature[36: 56])), + address(bytes20(signature[36:56])), keccak256(enableData) ) ) ); - + validationData = _intersectValidationData( getKernelStorage().defaultValidator.validateSignature( enableDigest, signature[120 + enableDataLength:120 + enableDataLength + enableSignatureLength] @@ -141,7 +144,7 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { ); validationSig = signature[120 + enableDataLength + enableSignatureLength:]; getKernelStorage().execution[sig] = ExecutionDetail({ - executor: address(bytes20(signature[36: 56])), + executor: address(bytes20(signature[36:56])), validator: IKernelValidator(address(bytes20(signature[16:36]))), validUntil: uint48(bytes6(signature[4:10])), validAfter: uint48(bytes6(signature[10:16])) diff --git a/src/abstract/KernelStorage.sol b/src/abstract/KernelStorage.sol index 979044e1..c6a16317 100644 --- a/src/abstract/KernelStorage.sol +++ b/src/abstract/KernelStorage.sol @@ -94,17 +94,26 @@ contract KernelStorage { } // change storage - function setExecution(bytes4 _selector, address _executor, IKernelValidator _validator, uint48 _validUntil, uint48 _validAfter) - external - onlyFromEntryPointOrOwnerOrSelf - { + function setExecution( + bytes4 _selector, + address _executor, + IKernelValidator _validator, + uint48 _validUntil, + uint48 _validAfter + ) external onlyFromEntryPointOrOwnerOrSelf { getKernelStorage().execution[_selector] = ExecutionDetail({ - executor: _executor, validator: _validator, validUntil: _validUntil, validAfter: _validAfter + executor: _executor, + validator: _validator, + validUntil: _validUntil, + validAfter: _validAfter }); emit ExecutionChanged(_selector, _executor, address(_validator)); } - function setDefaultValidator(IKernelValidator _defaultValidator, bytes calldata _data) external onlyFromEntryPointOrOwnerOrSelf { + function setDefaultValidator(IKernelValidator _defaultValidator, bytes calldata _data) + external + onlyFromEntryPointOrOwnerOrSelf + { IKernelValidator oldValidator = getKernelStorage().defaultValidator; getKernelStorage().defaultValidator = _defaultValidator; emit DefaultValidatorChanged(address(oldValidator), address(_defaultValidator)); diff --git a/src/test/TestValidator.sol b/src/test/TestValidator.sol index 38ae6978..95299ef8 100644 --- a/src/test/TestValidator.sol +++ b/src/test/TestValidator.sol @@ -7,6 +7,7 @@ contract TestValidator is IKernelValidator { event TestValidateUserOp(bytes32 indexed opHash); event TestEnable(bytes data); event TestDisable(bytes data); + function validateSignature(bytes32, bytes calldata) external pure override returns (uint256) { return 0; } diff --git a/test/foundry/Kernel.test.sol b/test/foundry/Kernel.test.sol index 4ac96bdf..53f2253d 100644 --- a/test/foundry/Kernel.test.sol +++ b/test/foundry/Kernel.test.sol @@ -80,7 +80,8 @@ contract KernelTest is Test { TestValidator newValidator = new TestValidator(); bytes memory empty; UserOperation memory op = entryPoint.fillUserOp( - address(kernel), abi.encodeWithSelector(KernelStorage.setDefaultValidator.selector, address(newValidator), empty) + address(kernel), + abi.encodeWithSelector(KernelStorage.setDefaultValidator.selector, address(newValidator), empty) ); op.signature = abi.encodePacked(bytes4(0x00000000), entryPoint.signUserOpHash(vm, ownerKey, op)); UserOperation[] memory ops = new UserOperation[](1); @@ -104,7 +105,12 @@ contract KernelTest is Test { UserOperation memory op = entryPoint.fillUserOp( address(kernel), abi.encodeWithSelector( - KernelStorage.setExecution.selector, bytes4(0xdeadbeef), address(0xdead), address(0xbeef), uint48(0), uint48(0) + KernelStorage.setExecution.selector, + bytes4(0xdeadbeef), + address(0xdead), + address(0xbeef), + uint48(0), + uint48(0) ) ); op.signature = abi.encodePacked(bytes4(0x00000000), entryPoint.signUserOpHash(vm, ownerKey, op)); diff --git a/test/foundry/KernelExecution.test.sol b/test/foundry/KernelExecution.test.sol index ffdfa81d..b7999acf 100644 --- a/test/foundry/KernelExecution.test.sol +++ b/test/foundry/KernelExecution.test.sol @@ -61,20 +61,36 @@ contract KernelExecutionTest is Test { op.signature = abi.encodePacked(bytes4(0x00000001), entryPoint.signUserOpHash(vm, ownerKey, op)); ops[0] = op; - vm.expectRevert(abi.encodeWithSelector(IEntryPoint.FailedOp.selector, 0, string.concat("AA23 reverted: ", "kernel: mode disabled"))); + vm.expectRevert( + abi.encodeWithSelector( + IEntryPoint.FailedOp.selector, 0, string.concat("AA23 reverted: ", "kernel: mode disabled") + ) + ); entryPoint.handleOps(ops, beneficiary); } - function test_mode_1() external { + function test_mode_2() external { TestValidator testValidator = new TestValidator(); - UserOperation memory op = entryPoint.fillUserOp( + UserOperation memory op = entryPoint.fillUserOp( address(kernel), abi.encodeWithSelector(Kernel.execute.selector, address(0xdeadbeef), 1, "") ); - bytes32 digest = getTypedDataHash(address(kernel), Kernel.execute.selector, 0,0, address(testValidator), address(0), ""); + bytes32 digest = + getTypedDataHash(address(kernel), Kernel.execute.selector, 0, 0, address(testValidator), address(0), ""); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey, digest); - op.signature = abi.encodePacked(bytes4(0x00000001), uint48(0), uint48(0), address(testValidator), address(0), uint256(0), uint256(65), r,s,v); + op.signature = abi.encodePacked( + bytes4(0x00000002), + uint48(0), + uint48(0), + address(testValidator), + address(0), + uint256(0), + uint256(65), + r, + s, + v + ); UserOperation[] memory ops = new UserOperation[](1); ops[0] = op; // vm.expectEmit(true, false, false, false); @@ -83,49 +99,52 @@ contract KernelExecutionTest is Test { } } - // computes the hash of a permit -function getStructHash(bytes4 sig, uint48 validUntil, uint48 validAfter, address validator, address executor, bytes memory enableData) - pure - returns (bytes32) -{ - return - keccak256( - abi.encode( - keccak256("ValidatorApproved(bytes4 sig,uint256 validatorData,address executor,bytes enableData)"), - bytes4(sig), - uint256(uint256(uint160(validator)) | (uint256(validAfter) << 160) | (uint256(validUntil) << (48 + 160))), - executor, - keccak256(enableData) - ) - ); +function getStructHash( + bytes4 sig, + uint48 validUntil, + uint48 validAfter, + address validator, + address executor, + bytes memory enableData +) pure returns (bytes32) { + return keccak256( + abi.encode( + keccak256("ValidatorApproved(bytes4 sig,uint256 validatorData,address executor,bytes enableData)"), + bytes4(sig), + uint256(uint256(uint160(validator)) | (uint256(validAfter) << 160) | (uint256(validUntil) << (48 + 160))), + executor, + keccak256(enableData) + ) + ); } // computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer -function getTypedDataHash(address sender, bytes4 sig, uint48 validUntil, uint48 validAfter, address validator, address executor, bytes memory enableData) +function getTypedDataHash( + address sender, + bytes4 sig, + uint48 validUntil, + uint48 validAfter, + address validator, + address executor, + bytes memory enableData +) view returns (bytes32) { + return keccak256( + abi.encodePacked( + "\x19\x01", + _buildDomainSeparator("Kernel", "0.0.2", sender), + getStructHash(sig, validUntil, validAfter, validator, executor, enableData) + ) + ); +} + +function _buildDomainSeparator(string memory name, string memory version, address verifyingContract) view returns (bytes32) { - return - keccak256( - abi.encodePacked( - "\x19\x01", - _buildDomainSeparator("Kernel", "0.0.2", sender), - getStructHash(sig, validUntil, validAfter, validator, executor, enableData) - ) - ); -} - -function _buildDomainSeparator( - string memory name, - string memory version, - address verifyingContract -) view returns (bytes32) { bytes32 hashedName = keccak256(bytes(name)); bytes32 hashedVersion = keccak256(bytes(version)); - bytes32 typeHash = keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - ); + bytes32 typeHash = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); return keccak256(abi.encode(typeHash, hashedName, hashedVersion, block.chainid, address(verifyingContract))); } From 6f1a978c4145575ad706432addf69870846afb92 Mon Sep 17 00:00:00 2001 From: leekt Date: Mon, 15 May 2023 06:07:36 +0900 Subject: [PATCH 09/18] test for executor --- src/Kernel.sol | 54 +++++++++++++-------------- src/test/TestExecutor.sol | 7 ++++ test/foundry/KernelExecution.test.sol | 36 ++++++++++++++++++ 3 files changed, 69 insertions(+), 28 deletions(-) create mode 100644 src/test/TestExecutor.sol diff --git a/src/Kernel.sol b/src/Kernel.sol index 5c401b2e..5a5e2ea4 100644 --- a/src/Kernel.sol +++ b/src/Kernel.sol @@ -76,39 +76,37 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { // mode == 0x00000000 use sudo validator // mode & 0x00000001 == 0x00000001 use given validator // mode & 0x00000002 == 0x00000002 enable validator + UserOperation memory op = userOp; + IKernelValidator validator; + bytes4 sig = bytes4(userOp.callData[0:4]); if (mode == 0x00000000) { // sudo mode (use default validator) - UserOperation memory op = userOp; + op = userOp; op.signature = userOp.signature[4:]; - validationData = getKernelStorage().defaultValidator.validateUserOp(op, userOpHash, missingAccountFunds); - } else { - UserOperation memory op = userOp; - bytes4 sig = bytes4(userOp.callData[0:4]); - IKernelValidator validator; - if (mode == 0x00000001) { - ExecutionDetail storage detail = getKernelStorage().execution[sig]; - validator = detail.validator; - if (address(validator) == address(0)) { - validator = getKernelStorage().defaultValidator; - } - op.signature = userOp.signature[4:]; - validationData = (uint256(detail.validAfter) << 160) | (uint256(detail.validUntil) << (48 + 160)); - } else if (mode & 0x00000002 == 0x00000002) { - // use given validator - // userOp.signature[4:10] = validUntil, - // userOp.signature[10:16] = validAfter, - // userOp.signature[16:36] = validator address, - validator = IKernelValidator(address(bytes20(userOp.signature[16:36]))); - bytes calldata enableData; - bytes calldata remainSig; - (validationData, enableData, remainSig) = _approveValidator(sig, userOp.signature); - validator.enable(enableData); - } else { - return SIG_VALIDATION_FAILED; + validator = getKernelStorage().defaultValidator; + } else if (mode == 0x00000001) { + ExecutionDetail storage detail = getKernelStorage().execution[sig]; + validator = detail.validator; + if (address(validator) == address(0)) { + validator = getKernelStorage().defaultValidator; } - validationData = - _intersectValidationData(validationData, validator.validateUserOp(op, userOpHash, missingAccountFunds)); + op.signature = userOp.signature[4:]; + validationData = (uint256(detail.validAfter) << 160) | (uint256(detail.validUntil) << (48 + 160)); + } else if (mode == 0x00000002) { + // use given validator + // userOp.signature[4:10] = validUntil, + // userOp.signature[10:16] = validAfter, + // userOp.signature[16:36] = validator address, + validator = IKernelValidator(address(bytes20(userOp.signature[16:36]))); + bytes calldata enableData; + bytes calldata remainSig; + (validationData, enableData, remainSig) = _approveValidator(sig, userOp.signature); + validator.enable(enableData); + } else { + return SIG_VALIDATION_FAILED; } + validationData = + _intersectValidationData(validationData, validator.validateUserOp(op, userOpHash, missingAccountFunds)); if (missingAccountFunds > 0) { // we are going to assume signature is valid at this point (bool success,) = msg.sender.call{value: missingAccountFunds}(""); diff --git a/src/test/TestExecutor.sol b/src/test/TestExecutor.sol new file mode 100644 index 00000000..98ddb59f --- /dev/null +++ b/src/test/TestExecutor.sol @@ -0,0 +1,7 @@ +pragma solidity ^0.8.0; + +contract TestExecutor { + function doNothing() external { + // do nothing + } +} diff --git a/test/foundry/KernelExecution.test.sol b/test/foundry/KernelExecution.test.sol index b7999acf..345e155d 100644 --- a/test/foundry/KernelExecution.test.sol +++ b/test/foundry/KernelExecution.test.sol @@ -6,6 +6,7 @@ import "src/validator/ECDSAValidator.sol"; import "src/factory/EIP1967Proxy.sol"; // test artifacts import "src/test/TestValidator.sol"; +import "src/test/TestExecutor.sol"; // test utils import "forge-std/Test.sol"; import {ERC4337Utils} from "./ERC4337Utils.sol"; @@ -97,6 +98,41 @@ contract KernelExecutionTest is Test { // emit TestValidator.TestValidateUserOp(opHash); entryPoint.handleOps(ops, beneficiary); } + + function test_mode_2_1() external { + TestValidator testValidator = new TestValidator(); + TestExecutor testExecutor = new TestExecutor(); + UserOperation memory op = + entryPoint.fillUserOp(address(kernel), abi.encodeWithSelector(TestExecutor.doNothing.selector)); + + bytes32 digest = getTypedDataHash( + address(kernel), TestExecutor.doNothing.selector, 0, 0, address(testValidator), address(testExecutor), "" + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey, digest); + + op.signature = abi.encodePacked( + bytes4(0x00000002), + uint48(0), + uint48(0), + address(testValidator), + address(testExecutor), + uint256(0), + uint256(65), + r, + s, + v + ); + UserOperation[] memory ops = new UserOperation[](1); + ops[0] = op; + // vm.expectEmit(true, false, false, false); + // emit TestValidator.TestValidateUserOp(opHash); + entryPoint.handleOps(ops, beneficiary); + op = entryPoint.fillUserOp(address(kernel), abi.encodeWithSelector(TestExecutor.doNothing.selector)); + // registered + op.signature = abi.encodePacked(bytes4(0x00000001)); + ops[0] = op; + entryPoint.handleOps(ops, beneficiary); + } } // computes the hash of a permit From 012ff4e25f708cd51f11d41bc2a45fc7a53d4989 Mon Sep 17 00:00:00 2001 From: leekt Date: Mon, 15 May 2023 06:43:24 +0900 Subject: [PATCH 10/18] deploying more tests --- foundry.toml | 1 - src/Kernel.sol | 4 +-- src/test/TestExecutor.sol | 3 ++ src/test/TestValidator.sol | 7 ++++- test/foundry/KernelExecution.test.sol | 43 +++++++++++++++++++++++---- 5 files changed, 48 insertions(+), 10 deletions(-) diff --git a/foundry.toml b/foundry.toml index 3c1604e9..21caefb3 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,6 +3,5 @@ src = 'src' out = 'out' libs = ['lib'] remappings = ['account-abstraction/=lib/account-abstraction/contracts/'] -via_ir = true # See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/src/Kernel.sol b/src/Kernel.sol index 5a5e2ea4..42a1172d 100644 --- a/src/Kernel.sol +++ b/src/Kernel.sol @@ -74,8 +74,8 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { bytes4 mode = bytes4(userOp.signature[0:4]); // mode == 00..00 use validators require(mode & getKernelStorage().disabledMode == 0x00000000, "kernel: mode disabled"); // mode == 0x00000000 use sudo validator - // mode & 0x00000001 == 0x00000001 use given validator - // mode & 0x00000002 == 0x00000002 enable validator + // mode == 0x00000001 use given validator + // mode == 0x00000002 enable validator UserOperation memory op = userOp; IKernelValidator validator; bytes4 sig = bytes4(userOp.callData[0:4]); diff --git a/src/test/TestExecutor.sol b/src/test/TestExecutor.sol index 98ddb59f..f720a674 100644 --- a/src/test/TestExecutor.sol +++ b/src/test/TestExecutor.sol @@ -1,7 +1,10 @@ +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract TestExecutor { + event TestExecutorDoNothing(); function doNothing() external { // do nothing + emit TestExecutorDoNothing(); } } diff --git a/src/test/TestValidator.sol b/src/test/TestValidator.sol index 95299ef8..e7363db1 100644 --- a/src/test/TestValidator.sol +++ b/src/test/TestValidator.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import "src/validator/IValidator.sol"; +import "forge-std/console.sol"; contract TestValidator is IKernelValidator { event TestValidateUserOp(bytes32 indexed opHash); @@ -12,8 +13,12 @@ contract TestValidator is IKernelValidator { return 0; } - function validateUserOp(UserOperation calldata, bytes32 userOpHash, uint256) external override returns (uint256) { + function validateUserOp(UserOperation calldata op, bytes32 userOpHash, uint256) external override returns (uint256) { emit TestValidateUserOp(userOpHash); + console.log("sender is %s", op.sender); + console.log("nonce is %s", op.nonce); + console.log("signature is "); + console.logBytes(op.signature); return 0; } diff --git a/test/foundry/KernelExecution.test.sol b/test/foundry/KernelExecution.test.sol index 345e155d..b090fdba 100644 --- a/test/foundry/KernelExecution.test.sol +++ b/test/foundry/KernelExecution.test.sol @@ -70,14 +70,25 @@ contract KernelExecutionTest is Test { entryPoint.handleOps(ops, beneficiary); } + function test_sudo() external { + UserOperation memory op = + entryPoint.fillUserOp(address(kernel), abi.encodeWithSelector(TestExecutor.doNothing.selector)); + op.signature = abi.encodePacked(bytes4(0x00000000), entryPoint.signUserOpHash(vm, ownerKey, op)); + UserOperation[] memory ops = new UserOperation[](1); + ops[0] = op; + logGas(op); + entryPoint.handleOps(ops, beneficiary); + } + function test_mode_2() external { TestValidator testValidator = new TestValidator(); - UserOperation memory op = entryPoint.fillUserOp( - address(kernel), abi.encodeWithSelector(Kernel.execute.selector, address(0xdeadbeef), 1, "") - ); + TestExecutor testExecutor = new TestExecutor(); + UserOperation memory op = + entryPoint.fillUserOp(address(kernel), abi.encodeWithSelector(TestExecutor.doNothing.selector)); - bytes32 digest = - getTypedDataHash(address(kernel), Kernel.execute.selector, 0, 0, address(testValidator), address(0), ""); + bytes32 digest = getTypedDataHash( + address(kernel), TestExecutor.doNothing.selector, 0, 0, address(testValidator), address(testExecutor), "" + ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey, digest); op.signature = abi.encodePacked( @@ -85,7 +96,7 @@ contract KernelExecutionTest is Test { uint48(0), uint48(0), address(testValidator), - address(0), + address(testExecutor), uint256(0), uint256(65), r, @@ -96,6 +107,8 @@ contract KernelExecutionTest is Test { ops[0] = op; // vm.expectEmit(true, false, false, false); // emit TestValidator.TestValidateUserOp(opHash); + logGas(op); + entryPoint.handleOps(ops, beneficiary); } @@ -131,8 +144,26 @@ contract KernelExecutionTest is Test { // registered op.signature = abi.encodePacked(bytes4(0x00000001)); ops[0] = op; + logGas(op); entryPoint.handleOps(ops, beneficiary); } + + function logGas(UserOperation memory op) internal returns(uint256 used) { + try this.consoleGasUsage(op) { + revert("should revert"); + } catch Error(string memory reason) { + used = abi.decode(bytes(reason), (uint256)); + console.log("validation gas usage :", used); + } + } + + function consoleGasUsage(UserOperation memory op) external { + uint256 gas = gasleft(); + vm.startPrank(address(entryPoint)); + kernel.validateUserOp(op, entryPoint.getUserOpHash(op), 0); + vm.stopPrank(); + revert(string(abi.encodePacked(gas - gasleft()))); + } } // computes the hash of a permit From bc57b2e6455d7a5f8fd57d716e5d1a891df6eea4 Mon Sep 17 00:00:00 2001 From: leekt Date: Mon, 15 May 2023 07:42:17 +0900 Subject: [PATCH 11/18] erc165 nft session key poc --- src/Kernel.sol | 1 + src/test/TestERC721.sol | 12 +++++ src/test/TestValidator.sol | 6 +-- src/utils/KernelHelper.sol | 2 + src/validator/ECDSAValidator.sol | 3 +- src/validator/ERC165SessionKeyValidator.sol | 55 +++++++++++++++++++++ test/foundry/KernelExecution.test.sol | 49 ++++++++++++++++++ 7 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 src/test/TestERC721.sol create mode 100644 src/validator/ERC165SessionKeyValidator.sol diff --git a/src/Kernel.sol b/src/Kernel.sol index 42a1172d..9a363437 100644 --- a/src/Kernel.sol +++ b/src/Kernel.sol @@ -102,6 +102,7 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { bytes calldata remainSig; (validationData, enableData, remainSig) = _approveValidator(sig, userOp.signature); validator.enable(enableData); + op.signature = remainSig; } else { return SIG_VALIDATION_FAILED; } diff --git a/src/test/TestERC721.sol b/src/test/TestERC721.sol new file mode 100644 index 00000000..7d0ff3a0 --- /dev/null +++ b/src/test/TestERC721.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; + +contract TestERC721 is ERC721 { + constructor() ERC721("TestERC721", "TEST") {} + + function mint(address _to, uint256 _id) external { + _mint(_to, _id); + } +} \ No newline at end of file diff --git a/src/test/TestValidator.sol b/src/test/TestValidator.sol index e7363db1..a59d4cff 100644 --- a/src/test/TestValidator.sol +++ b/src/test/TestValidator.sol @@ -13,12 +13,8 @@ contract TestValidator is IKernelValidator { return 0; } - function validateUserOp(UserOperation calldata op, bytes32 userOpHash, uint256) external override returns (uint256) { + function validateUserOp(UserOperation calldata, bytes32 userOpHash, uint256) external override returns (uint256) { emit TestValidateUserOp(userOpHash); - console.log("sender is %s", op.sender); - console.log("nonce is %s", op.nonce); - console.log("signature is "); - console.logBytes(op.signature); return 0; } diff --git a/src/utils/KernelHelper.sol b/src/utils/KernelHelper.sol index df441972..ef6ff33d 100644 --- a/src/utils/KernelHelper.sol +++ b/src/utils/KernelHelper.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +uint256 constant SIG_VALIDATION_FAILED = 1; + function _intersectValidationData(uint256 a, uint256 b) pure returns (uint256 validationData) { require(uint160(a) == uint160(b), "account: different aggregator"); uint48 validAfterA = uint48(a >> 160); diff --git a/src/validator/ECDSAValidator.sol b/src/validator/ECDSAValidator.sol index 8be2bf50..d4d9110c 100644 --- a/src/validator/ECDSAValidator.sol +++ b/src/validator/ECDSAValidator.sol @@ -5,13 +5,12 @@ pragma solidity ^0.8.0; import "./IValidator.sol"; import "openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; import "forge-std/console.sol"; +import "src/utils/KernelHelper.sol"; struct ECDSAValidatorStorage { address owner; } -uint256 constant SIG_VALIDATION_FAILED = 1; - contract ECDSAValidator is IKernelValidator { event OwnerChanged(address indexed kernel, address indexed oldOwner, address indexed newOwner); diff --git a/src/validator/ERC165SessionKeyValidator.sol b/src/validator/ERC165SessionKeyValidator.sol new file mode 100644 index 00000000..01004665 --- /dev/null +++ b/src/validator/ERC165SessionKeyValidator.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./IValidator.sol"; +import "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; +import "openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; +import "src/utils/KernelHelper.sol"; + +import "forge-std/console.sol"; + + +// idea, we can make this merkle root +struct ERC165SessionKeyStorage { + bool enabled; + bytes4 selector; + bytes4 interfaceId; + uint48 validUntil; + uint48 validAfter; + uint32 addressOffset; +} + +contract ERC165SessionKeyValidator is IKernelValidator { + mapping(address => mapping(address => ERC165SessionKeyStorage)) public sessionKeys; + function enable(bytes calldata _data) external { + address sessionKey = address(bytes20(_data[0:20])); + bytes4 interfaceId = bytes4(_data[20:24]); + bytes4 selector = bytes4(_data[24:28]); + uint48 validUntil = uint48(bytes6(_data[28:34])); + uint48 validAfter = uint48(bytes6(_data[34:40])); + uint32 addressOffset = uint32(bytes4(_data[40:44])); + sessionKeys[msg.sender][sessionKey] = ERC165SessionKeyStorage(true, selector, interfaceId, validUntil, validAfter, addressOffset); + } + + function disable(bytes calldata _data) external { + address sessionKey = address(bytes20(_data[0:20])); + delete sessionKeys[msg.sender][sessionKey]; + } + + function validateSignature(bytes32, bytes calldata) external pure override returns(uint256) { + revert("not implemented"); + } + + function validateUserOp(UserOperation calldata _userOp, bytes32 _userOpHash, uint256) external view returns(uint256){ + bytes32 hash = ECDSA.toEthSignedMessageHash(_userOpHash); + address recovered = ECDSA.recover(hash, _userOp.signature); + ERC165SessionKeyStorage storage sessionKey = sessionKeys[_userOp.sender][recovered]; + if (!sessionKey.enabled) { + return SIG_VALIDATION_FAILED; + } + require(bytes4(_userOp.callData[0:4]) == sessionKey.selector, "not supported selector"); + address token = address(bytes20(_userOp.callData[sessionKey.addressOffset:sessionKey.addressOffset+20])); + require(IERC165(token).supportsInterface(sessionKey.interfaceId), "does not support interface"); + return (uint256(sessionKey.validAfter) << 160) | (uint256(sessionKey.validUntil) << (48 + 160)); + } +} \ No newline at end of file diff --git a/test/foundry/KernelExecution.test.sol b/test/foundry/KernelExecution.test.sol index b090fdba..0da59297 100644 --- a/test/foundry/KernelExecution.test.sol +++ b/test/foundry/KernelExecution.test.sol @@ -7,9 +7,13 @@ import "src/factory/EIP1967Proxy.sol"; // test artifacts import "src/test/TestValidator.sol"; import "src/test/TestExecutor.sol"; +import "src/test/TestERC721.sol"; // test utils import "forge-std/Test.sol"; import {ERC4337Utils} from "./ERC4337Utils.sol"; +// test actions/validators +import "src/validator/ERC165SessionKeyValidator.sol"; +import "src/actions/ERC721Actions.sol"; using ERC4337Utils for EntryPoint; @@ -148,6 +152,51 @@ contract KernelExecutionTest is Test { entryPoint.handleOps(ops, beneficiary); } + function test_mode_2_erc165() external { + ERC165SessionKeyValidator sessionKeyValidator = new ERC165SessionKeyValidator(); + ERC721Actions action = new ERC721Actions(); + TestERC721 erc721 = new TestERC721(); + erc721.mint(address(kernel), 0); + UserOperation memory op = + entryPoint.fillUserOp(address(kernel), abi.encodeWithSelector(ERC721Actions.transferERC721Action.selector, address(erc721), 0, address(0xdead))); + address sessionKeyAddr; + uint256 sessionKeyPriv; + (sessionKeyAddr, sessionKeyPriv) = makeAddrAndKey("sessionKey"); + bytes memory enableData = abi.encodePacked(sessionKeyAddr, type(IERC721).interfaceId, ERC721Actions.transferERC721Action.selector, uint48(0), uint48(0), uint32(16)); + { + bytes32 digest = getTypedDataHash( + address(kernel), ERC721Actions.transferERC721Action.selector, 0, 0, address(sessionKeyValidator), address(action), + enableData + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey, digest); + + op.signature = abi.encodePacked( + bytes4(0x00000002), + uint48(0), + uint48(0), + address(sessionKeyValidator), + address(action), + uint256(enableData.length), + enableData, + uint256(65), + r, + s, + v + ); + } + + op.signature = bytes.concat( + op.signature, + entryPoint.signUserOpHash(vm, sessionKeyPriv, op) + ); + + UserOperation[] memory ops = new UserOperation[](1); + ops[0] = op; + entryPoint.handleOps(ops, beneficiary); + + assertEq(erc721.ownerOf(0), address(0xdead)); + } + function logGas(UserOperation memory op) internal returns(uint256 used) { try this.consoleGasUsage(op) { revert("should revert"); From 00cfa864a666d8aa69fb9da28eef08b167ccedd3 Mon Sep 17 00:00:00 2001 From: leekt Date: Wed, 17 May 2023 22:55:23 +0900 Subject: [PATCH 12/18] updated comments --- src/Kernel.sol | 37 +++++++++------ src/abstract/KernelStorage.sol | 66 +++++++++++++++++++-------- test/foundry/Kernel.test.sol | 27 ++++------- test/foundry/KernelExecution.test.sol | 31 ++++++------- 4 files changed, 90 insertions(+), 71 deletions(-) diff --git a/src/Kernel.sol b/src/Kernel.sol index 9a363437..4c6c377a 100644 --- a/src/Kernel.sol +++ b/src/Kernel.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +// Importing external libraries and contracts import "openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; import "account-abstraction/core/Helpers.sol"; import "account-abstraction/interfaces/IAccount.sol"; @@ -11,8 +12,6 @@ import "./abstract/Compatibility.sol"; import "./abstract/KernelStorage.sol"; import "./utils/KernelHelper.sol"; -import "forge-std/console.sol"; - /// @title Kernel /// @author taek /// @notice wallet kernel for minimal wallet functionality @@ -21,8 +20,11 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { string public constant version = "0.0.2"; + /// @dev Sets up the EIP712 and KernelStorage with the provided entry point constructor(IEntryPoint _entryPoint) EIP712(name, version) KernelStorage(_entryPoint) {} + /// @notice Accepts incoming Ether transactions and calls from the EntryPoint contract + /// @dev This function will delegate any call to the appropriate executor based on the function signature. fallback() external payable { require(msg.sender == address(entryPoint), "account: not from entrypoint"); bytes4 sig = msg.sig; @@ -37,12 +39,12 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { } } - /// @notice execute function call to external contract - /// @dev this function will execute function call to external contract - /// @param to target contract address - /// @param value value to be sent - /// @param data data to be sent - /// @param operation operation type (call or delegatecall) + /// @notice Executes a function call to an external contract + /// @dev The type of operation (call or delegatecall) is specified as an argument. + /// @param to The address of the target contract + /// @param value The amount of Ether to send + /// @param data The call data to be sent + /// @param operation The type of operation (call or delegatecall) function execute(address to, uint256 value, bytes calldata data, Operation operation) external { require(msg.sender == address(entryPoint), "account: not from entrypoint"); bool success; @@ -59,12 +61,12 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { } } - /// @notice validate user operation - /// @dev this function will validate user operation and be called by EntryPoint - /// @param userOp user operation - /// @param userOpHash user operation hash - /// @param missingAccountFunds funds needed to be reimbursed - /// @return validationData validation data + /// @notice Validates a user operation based on its mode + /// @dev This function will validate user operation and be called by EntryPoint + /// @param userOp The user operation to be validated + /// @param userOpHash The hash of the user operation + /// @param missingAccountFunds The funds needed to be reimbursed + /// @return validationData The data used for validation function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) external returns (uint256 validationData) @@ -150,7 +152,12 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { }); return (validationData, signature[88:88 + enableDataLength], validationSig); } - + + /// @notice Checks if a signature is valid + /// @dev This function checks if a signature is valid based on the hash of the data signed. + /// @param hash The hash of the data that was signed + /// @param signature The signature to be validated + /// @return The magic value 0x1626ba7e if the signature is valid, otherwise returns 0xffffffff. function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { uint256 validationData = getKernelStorage().defaultValidator.validateSignature(hash, signature); ValidationData memory data = _parseValidationData(validationData); diff --git a/src/abstract/KernelStorage.sol b/src/abstract/KernelStorage.sol index c6a16317..f3872390 100644 --- a/src/abstract/KernelStorage.sol +++ b/src/abstract/KernelStorage.sol @@ -1,34 +1,42 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +// Importing necessary interfaces import "account-abstraction/interfaces/IEntryPoint.sol"; import "src/validator/IValidator.sol"; +// Defining a struct for execution details struct ExecutionDetail { - uint48 validUntil; - uint48 validAfter; - address executor; - IKernelValidator validator; + uint48 validUntil; // Until what time is this execution valid + uint48 validAfter; // After what time is this execution valid + address executor; // Who is the executor of this execution + IKernelValidator validator; // The validator for this execution } +// Defining a struct for wallet kernel storage struct WalletKernelStorage { - bytes32 __deprecated; - IKernelValidator defaultValidator; - bytes4 disabledMode; - mapping(bytes4 => ExecutionDetail) execution; + bytes32 __deprecated; // A deprecated field + bytes4 disabledMode; // Mode which is currently disabled + uint48 lastDisabledTime; // Last time when a mode was disabled + IKernelValidator defaultValidator; // Default validator for the wallet + mapping(bytes4 => ExecutionDetail) execution; // Mapping of function selectors to execution details } +/// @title Kernel Storage Contract +/// @author Your Name +/// @notice This contract serves as the storage module for the Kernel contract. +/// @dev This contract should only be used by the main Kernel contract. contract KernelStorage { - uint256 internal constant SIG_VALIDATION_FAILED = 1; + uint256 internal constant SIG_VALIDATION_FAILED = 1; // Signature validation failed error code - IEntryPoint public immutable entryPoint; + IEntryPoint public immutable entryPoint; // The entry point of the contract + // Event declarations event Upgraded(address indexed newImplementation); event DefaultValidatorChanged(address indexed oldValidator, address indexed newValidator); event ExecutionChanged(bytes4 indexed selector, address indexed executor, address indexed validator); - // modifier for checking if the sender is the entrypoint or - // the account itself + // Modifier to check if the function is called by the entry point, the contract itself or the owner modifier onlyFromEntryPointOrOwnerOrSelf() { require( msg.sender == address(entryPoint) || msg.sender == address(this), @@ -37,14 +45,14 @@ contract KernelStorage { _; } + /// @param _entryPoint The address of the EntryPoint contract + /// @dev Sets up the EntryPoint contract address constructor(IEntryPoint _entryPoint) { entryPoint = _entryPoint; getKernelStorage().defaultValidator = IKernelValidator(address(1)); } - /// @notice initialize wallet kernel - /// @dev this function should be called only once, implementation initialize is blocked by owner = address(1) - /// @param _defaultValidator owner address + // Function to initialize the wallet kernel function initialize(IKernelValidator _defaultValidator, bytes calldata _data) external { WalletKernelStorage storage ws = getKernelStorage(); require(address(ws.defaultValidator) == address(0), "account: already initialized"); @@ -53,9 +61,7 @@ contract KernelStorage { _defaultValidator.enable(_data); } - /// @notice get wallet kernel storage - /// @dev used to get wallet kernel storage - /// @return ws wallet kernel storage, consists of owner and nonces + // Function to get the wallet kernel storage function getKernelStorage() internal pure returns (WalletKernelStorage storage ws) { bytes32 storagePosition = bytes32(uint256(keccak256("zerodev.kernel")) - 1); assembly { @@ -63,6 +69,7 @@ contract KernelStorage { } } + // Function to upgrade the contract to a new implementation function upgradeTo(address _newImplementation) external onlyFromEntryPointOrOwnerOrSelf { bytes32 slot = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; assembly { @@ -71,7 +78,7 @@ contract KernelStorage { emit Upgraded(_newImplementation); } - // nonce from entrypoint + // Functions to get the nonce from the entry point function getNonce() public view virtual returns (uint256) { return entryPoint.getNonce(address(this), 0); } @@ -80,6 +87,7 @@ contract KernelStorage { return entryPoint.getNonce(address(this), key); } + // query storage function getDefaultValidator() public view returns (IKernelValidator) { return getKernelStorage().defaultValidator; @@ -89,11 +97,25 @@ contract KernelStorage { return getKernelStorage().disabledMode; } + function getLastDisabledTime() public view returns (uint48) { + return getKernelStorage().lastDisabledTime; + } + + /// @notice Returns the execution details for a specific function signature + /// @dev This function can be used to get execution details for a specific function signature + /// @param _selector The function signature + /// @return ExecutionDetail struct containing the execution details function getExecution(bytes4 _selector) public view returns (ExecutionDetail memory) { return getKernelStorage().execution[_selector]; } - // change storage + /// @notice Changes the execution details for a specific function selector + /// @dev This function can only be called from the EntryPoint contract, the contract owner, or itself + /// @param _selector The selector of the function for which execution details are being set + /// @param _executor The executor to be associated with the function selector + /// @param _validator The validator contract that will be responsible for validating operations associated with this function selector + /// @param _validUntil The timestamp until which the execution details are valid + /// @param _validAfter The timestamp after which the execution details are valid function setExecution( bytes4 _selector, address _executor, @@ -120,7 +142,11 @@ contract KernelStorage { _defaultValidator.enable(_data); } + /// @notice Updates the disabled mode + /// @dev This function can be used to update the disabled mode + /// @param _disableFlag The new disabled mode function disableMode(bytes4 _disableFlag) external onlyFromEntryPointOrOwnerOrSelf { getKernelStorage().disabledMode = _disableFlag; + getKernelStorage().lastDisabledTime = uint48(block.timestamp); } } diff --git a/test/foundry/Kernel.test.sol b/test/foundry/Kernel.test.sol index 53f2253d..9365ff0a 100644 --- a/test/foundry/Kernel.test.sol +++ b/test/foundry/Kernel.test.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import "src/factory/KernelFactory.sol"; import "src/Kernel.sol"; import "src/validator/ECDSAValidator.sol"; import "src/factory/EIP1967Proxy.sol"; @@ -13,8 +14,8 @@ import {ERC4337Utils} from "./ERC4337Utils.sol"; using ERC4337Utils for EntryPoint; contract KernelTest is Test { - Kernel implementation; Kernel kernel; + KernelFactory factory; EntryPoint entryPoint; ECDSAValidator validator; address owner; @@ -24,23 +25,10 @@ contract KernelTest is Test { function setUp() public { (owner, ownerKey) = makeAddrAndKey("owner"); entryPoint = new EntryPoint(); - implementation = new Kernel(entryPoint); + factory = new KernelFactory(entryPoint); validator = new ECDSAValidator(); - kernel = Kernel( - payable( - address( - new EIP1967Proxy( - address(implementation), - abi.encodeWithSelector( - implementation.initialize.selector, - validator, - abi.encodePacked(owner) - ) - ) - ) - ) - ); + kernel = Kernel(payable(address(factory.createAccount(owner, 0)))); vm.deal(address(kernel), 1e30); beneficiary = payable(address(makeAddr("beneficiary"))); } @@ -55,9 +43,9 @@ contract KernelTest is Test { payable( address( new EIP1967Proxy( - address(implementation), + address(factory.kernelTemplate()), abi.encodeWithSelector( - implementation.initialize.selector, + KernelStorage.initialize.selector, validator, abi.encodePacked(owner) ) @@ -91,8 +79,9 @@ contract KernelTest is Test { } function test_disable_mode() external { + bytes memory empty; UserOperation memory op = entryPoint.fillUserOp( - address(kernel), abi.encodeWithSelector(KernelStorage.disableMode.selector, bytes4(0x00000001)) + address(kernel), abi.encodeWithSelector(KernelStorage.disableMode.selector, bytes4(0x00000001), address(0), empty) ); op.signature = abi.encodePacked(bytes4(0x00000000), entryPoint.signUserOpHash(vm, ownerKey, op)); UserOperation[] memory ops = new UserOperation[](1); diff --git a/test/foundry/KernelExecution.test.sol b/test/foundry/KernelExecution.test.sol index 0da59297..d9a4d94a 100644 --- a/test/foundry/KernelExecution.test.sol +++ b/test/foundry/KernelExecution.test.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import "src/Kernel.sol"; import "src/validator/ECDSAValidator.sol"; import "src/factory/EIP1967Proxy.sol"; +import "src/factory/KernelFactory.sol"; // test artifacts import "src/test/TestValidator.sol"; import "src/test/TestExecutor.sol"; @@ -18,8 +19,8 @@ import "src/actions/ERC721Actions.sol"; using ERC4337Utils for EntryPoint; contract KernelExecutionTest is Test { - Kernel implementation; Kernel kernel; + KernelFactory factory; EntryPoint entryPoint; ECDSAValidator validator; address owner; @@ -29,30 +30,18 @@ contract KernelExecutionTest is Test { function setUp() public { (owner, ownerKey) = makeAddrAndKey("owner"); entryPoint = new EntryPoint(); - implementation = new Kernel(entryPoint); + factory = new KernelFactory(entryPoint); validator = new ECDSAValidator(); - kernel = Kernel( - payable( - address( - new EIP1967Proxy( - address(implementation), - abi.encodeWithSelector( - implementation.initialize.selector, - validator, - abi.encodePacked(owner) - ) - ) - ) - ) - ); + kernel = Kernel(payable(address(factory.createAccount(owner, 0)))); vm.deal(address(kernel), 1e30); beneficiary = payable(address(makeAddr("beneficiary"))); } function test_revert_when_mode_disabled() external { + bytes memory empty; UserOperation memory op = entryPoint.fillUserOp( - address(kernel), abi.encodeWithSelector(KernelStorage.disableMode.selector, bytes4(0x00000001)) + address(kernel), abi.encodeWithSelector(KernelStorage.disableMode.selector, bytes4(0x00000001), address(0), empty) ); op.signature = abi.encodePacked(bytes4(0x00000000), entryPoint.signUserOpHash(vm, ownerKey, op)); UserOperation[] memory ops = new UserOperation[](1); @@ -157,6 +146,7 @@ contract KernelExecutionTest is Test { ERC721Actions action = new ERC721Actions(); TestERC721 erc721 = new TestERC721(); erc721.mint(address(kernel), 0); + erc721.mint(address(kernel), 1); UserOperation memory op = entryPoint.fillUserOp(address(kernel), abi.encodeWithSelector(ERC721Actions.transferERC721Action.selector, address(erc721), 0, address(0xdead))); address sessionKeyAddr; @@ -192,6 +182,13 @@ contract KernelExecutionTest is Test { UserOperation[] memory ops = new UserOperation[](1); ops[0] = op; + logGas(op); + entryPoint.handleOps(ops, beneficiary); + + op = entryPoint.fillUserOp(address(kernel), abi.encodeWithSelector(ERC721Actions.transferERC721Action.selector, address(erc721), 1, address(0xdead))); + op.signature = abi.encodePacked(bytes4(0x00000001), entryPoint.signUserOpHash(vm, sessionKeyPriv, op)); + ops[0] = op; + logGas(op); entryPoint.handleOps(ops, beneficiary); assertEq(erc721.ownerOf(0), address(0xdead)); From 69792aaa3a8b1979981f71895f78be7196d53648 Mon Sep 17 00:00:00 2001 From: leekt Date: Wed, 17 May 2023 22:55:40 +0900 Subject: [PATCH 13/18] added kernel factory --- src/factory/KernelFactory.sol | 52 +++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/factory/KernelFactory.sol diff --git a/src/factory/KernelFactory.sol b/src/factory/KernelFactory.sol new file mode 100644 index 00000000..5ce2a049 --- /dev/null +++ b/src/factory/KernelFactory.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "openzeppelin-contracts/contracts/utils/Create2.sol"; +import "./EIP1967Proxy.sol"; +import "src/Kernel.sol"; +import "src/validator/ECDSAValidator.sol"; + +contract KernelFactory { + Kernel public immutable kernelTemplate; + + ECDSAValidator public immutable validator; + + event AccountCreated(address indexed account, address indexed owner, uint256 index); + + constructor(IEntryPoint _entryPoint) { + kernelTemplate = new Kernel(_entryPoint); + validator = new ECDSAValidator(); + } + + function createAccount(address _owner, uint256 _index) external returns (EIP1967Proxy proxy) { + bytes32 salt = keccak256(abi.encodePacked(_owner, _index)); + address addr = Create2.computeAddress( + salt, + keccak256( + abi.encodePacked( + type(EIP1967Proxy).creationCode, + abi.encode(address(kernelTemplate), abi.encodeCall(KernelStorage.initialize, (validator, abi.encodePacked(_owner)))) + ) + ) + ); + if (addr.code.length > 0) { + return EIP1967Proxy(payable(addr)); + } + proxy = + new EIP1967Proxy{salt: salt}(address(kernelTemplate), abi.encodeWithSelector(KernelStorage.initialize.selector, validator, abi.encodePacked(_owner))); + emit AccountCreated(address(proxy), _owner, _index); + } + + function getAccountAddress(address _owner, uint256 _index) public view returns (address) { + bytes32 salt = keccak256(abi.encodePacked(_owner, _index)); + return Create2.computeAddress( + salt, + keccak256( + abi.encodePacked( + type(EIP1967Proxy).creationCode, + abi.encode(address(kernelTemplate), abi.encodeCall(KernelStorage.initialize, (validator, abi.encodePacked(_owner)))) + ) + ) + ); + } +} \ No newline at end of file From 0a0a96d760a476d1b6be69aba15bda595b49598d Mon Sep 17 00:00:00 2001 From: leekt Date: Wed, 17 May 2023 22:55:54 +0900 Subject: [PATCH 14/18] KillSwitchValidator --- src/validator/KillSwitchValidator.sol | 60 +++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/validator/KillSwitchValidator.sol diff --git a/src/validator/KillSwitchValidator.sol b/src/validator/KillSwitchValidator.sol new file mode 100644 index 00000000..3760385c --- /dev/null +++ b/src/validator/KillSwitchValidator.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./IValidator.sol"; +import "openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; +import "src/utils/KernelHelper.sol"; +import "account-abstraction/core/Helpers.sol"; +import "src/Kernel.sol"; +import "./ECDSAValidator.sol"; + +struct KillSwitchValidatorStorage { + address owner; + address guardian; + uint48 pausedUntil; +} + +contract KillSwitchValidator is IKernelValidator { + mapping(address => KillSwitchValidatorStorage) public killSwitchValidatorStorage; + + function enable(bytes calldata enableData) external override { + killSwitchValidatorStorage[msg.sender].owner = address(bytes20(enableData[0:20])); + killSwitchValidatorStorage[msg.sender].guardian = address(bytes20(enableData[20:40])); + } + + function disable(bytes calldata) external override { + delete killSwitchValidatorStorage[msg.sender]; + } + + function validateSignature(bytes32, bytes calldata) external pure override returns (uint256) { + return SIG_VALIDATION_FAILED; + } + + function validateUserOp(UserOperation calldata _userOp, bytes32 _userOpHash, uint256) external override returns (uint256) { + address signer; + bytes calldata signature; + KillSwitchValidatorStorage storage validatorStorage = killSwitchValidatorStorage[_userOp.sender]; + if(_userOp.signature.length == 6 + 20 + 65) { + require(bytes4(_userOp.callData[0:4]) != KernelStorage.disableMode.selector); + signer = validatorStorage.guardian; + uint48 pausedUntil = uint48(bytes6(_userOp.signature[0:6])); + require(pausedUntil > validatorStorage.pausedUntil, "KillSwitchValidator: invalid pausedUntil"); + killSwitchValidatorStorage[_userOp.sender].pausedUntil = pausedUntil; + signature = _userOp.signature[6:71]; + } else { + signer = killSwitchValidatorStorage[_userOp.sender].owner; + signature = _userOp.signature; + } + if (signer == ECDSA.recover(_userOpHash, signature)) { // address(0) attack has been resolved in ECDSA library + return _packValidationData(false,0,validatorStorage.pausedUntil); + } + + bytes32 hash = ECDSA.toEthSignedMessageHash(_userOpHash); + address recovered = ECDSA.recover(hash, signature); + if (signer != recovered) { + return SIG_VALIDATION_FAILED; + } + return _packValidationData(false,0,validatorStorage.pausedUntil); + } +} \ No newline at end of file From f859ab71483514699264f500e2db07513d898fe7 Mon Sep 17 00:00:00 2001 From: leekt Date: Thu, 18 May 2023 06:42:13 +0900 Subject: [PATCH 15/18] setExecution will also enable the validator --- src/abstract/KernelStorage.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/abstract/KernelStorage.sol b/src/abstract/KernelStorage.sol index f3872390..25e46034 100644 --- a/src/abstract/KernelStorage.sol +++ b/src/abstract/KernelStorage.sol @@ -121,7 +121,8 @@ contract KernelStorage { address _executor, IKernelValidator _validator, uint48 _validUntil, - uint48 _validAfter + uint48 _validAfter, + bytes calldata _enableData ) external onlyFromEntryPointOrOwnerOrSelf { getKernelStorage().execution[_selector] = ExecutionDetail({ executor: _executor, @@ -129,6 +130,7 @@ contract KernelStorage { validUntil: _validUntil, validAfter: _validAfter }); + _validator.enable(_enableData); emit ExecutionChanged(_selector, _executor, address(_validator)); } From 1689adb1bd32ffbe2f9fba32b56e65411edcac78 Mon Sep 17 00:00:00 2001 From: leekt Date: Thu, 18 May 2023 19:53:23 +0900 Subject: [PATCH 16/18] updated killswitchvalidator to have validateSignature --- src/validator/KillSwitchValidator.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/validator/KillSwitchValidator.sol b/src/validator/KillSwitchValidator.sol index 3760385c..2897b745 100644 --- a/src/validator/KillSwitchValidator.sol +++ b/src/validator/KillSwitchValidator.sol @@ -27,8 +27,9 @@ contract KillSwitchValidator is IKernelValidator { delete killSwitchValidatorStorage[msg.sender]; } - function validateSignature(bytes32, bytes calldata) external pure override returns (uint256) { - return SIG_VALIDATION_FAILED; + function validateSignature(bytes32 hash, bytes calldata signature) external view override returns (uint256) { + KillSwitchValidatorStorage storage validatorStorage = killSwitchValidatorStorage[msg.sender]; + return _packValidationData(validatorStorage.owner == ECDSA.recover(hash, signature), 0, validatorStorage.pausedUntil); } function validateUserOp(UserOperation calldata _userOp, bytes32 _userOpHash, uint256) external override returns (uint256) { From 63dec3513ccc8fb30f06c71381914cc339edd034 Mon Sep 17 00:00:00 2001 From: leekt Date: Thu, 18 May 2023 21:17:56 +0900 Subject: [PATCH 17/18] factory can pass validator as input --- scripts/DeployKernel.s.sol | 2 +- src/factory/ECDSAKernelFactory.sol | 23 +++++++++++++++++++++++ src/factory/KernelFactory.sol | 20 ++++++++------------ test/foundry/Kernel.test.sol | 14 ++++++++++---- test/foundry/KernelExecution.test.sol | 6 +++++- 5 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 src/factory/ECDSAKernelFactory.sol diff --git a/scripts/DeployKernel.s.sol b/scripts/DeployKernel.s.sol index 57bc38d7..1791fa0a 100644 --- a/scripts/DeployKernel.s.sol +++ b/scripts/DeployKernel.s.sol @@ -1,6 +1,6 @@ pragma solidity ^0.8.0; -import "src/KernelFactory.sol"; +import "src/factory/KernelFactory.sol"; import "forge-std/Script.sol"; contract DeployKernel is Script { function run() public { diff --git a/src/factory/ECDSAKernelFactory.sol b/src/factory/ECDSAKernelFactory.sol new file mode 100644 index 00000000..ad0bbdc4 --- /dev/null +++ b/src/factory/ECDSAKernelFactory.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +import "./KernelFactory.sol"; +import "src/validator/ECDSAValidator.sol"; + +contract ECDSAKernelFactory { + KernelFactory immutable singletonFactory; + ECDSAValidator immutable validator; + + constructor(KernelFactory _singletonFactory, ECDSAValidator _validator) { + singletonFactory = _singletonFactory; + validator = _validator; + } + + function createAccount(address _owner, uint256 _index) external returns (EIP1967Proxy proxy) { + bytes memory data = abi.encodePacked(_owner); + proxy = singletonFactory.createAccount(validator, data, _index); + } + + function getAccountAddress(address _owner, uint256 _index) public view returns (address) { + bytes memory data = abi.encodePacked(_owner); + return singletonFactory.getAccountAddress(validator, data, _index); + } +} \ No newline at end of file diff --git a/src/factory/KernelFactory.sol b/src/factory/KernelFactory.sol index 5ce2a049..964847f4 100644 --- a/src/factory/KernelFactory.sol +++ b/src/factory/KernelFactory.sol @@ -9,23 +9,20 @@ import "src/validator/ECDSAValidator.sol"; contract KernelFactory { Kernel public immutable kernelTemplate; - ECDSAValidator public immutable validator; - event AccountCreated(address indexed account, address indexed owner, uint256 index); constructor(IEntryPoint _entryPoint) { kernelTemplate = new Kernel(_entryPoint); - validator = new ECDSAValidator(); } - function createAccount(address _owner, uint256 _index) external returns (EIP1967Proxy proxy) { - bytes32 salt = keccak256(abi.encodePacked(_owner, _index)); + function createAccount(IKernelValidator _validator, bytes calldata _data, uint256 _index) external returns (EIP1967Proxy proxy) { + bytes32 salt = keccak256(abi.encodePacked(_validator, _data, _index)); address addr = Create2.computeAddress( salt, keccak256( abi.encodePacked( type(EIP1967Proxy).creationCode, - abi.encode(address(kernelTemplate), abi.encodeCall(KernelStorage.initialize, (validator, abi.encodePacked(_owner)))) + abi.encode(address(kernelTemplate), abi.encodeCall(KernelStorage.initialize, (_validator, _data))) ) ) ); @@ -33,20 +30,19 @@ contract KernelFactory { return EIP1967Proxy(payable(addr)); } proxy = - new EIP1967Proxy{salt: salt}(address(kernelTemplate), abi.encodeWithSelector(KernelStorage.initialize.selector, validator, abi.encodePacked(_owner))); - emit AccountCreated(address(proxy), _owner, _index); + new EIP1967Proxy{salt: salt}(address(kernelTemplate), abi.encodeWithSelector(KernelStorage.initialize.selector, _validator, _data)); } - function getAccountAddress(address _owner, uint256 _index) public view returns (address) { - bytes32 salt = keccak256(abi.encodePacked(_owner, _index)); + function getAccountAddress(IKernelValidator _validator, bytes calldata _data, uint256 _index) public view returns (address) { + bytes32 salt = keccak256(abi.encodePacked(_validator, _data, _index)); return Create2.computeAddress( salt, keccak256( abi.encodePacked( type(EIP1967Proxy).creationCode, - abi.encode(address(kernelTemplate), abi.encodeCall(KernelStorage.initialize, (validator, abi.encodePacked(_owner)))) + abi.encode(address(kernelTemplate), abi.encodeCall(KernelStorage.initialize, (_validator, _data)) ) ) - ); + )); } } \ No newline at end of file diff --git a/test/foundry/Kernel.test.sol b/test/foundry/Kernel.test.sol index 9365ff0a..4ab6b887 100644 --- a/test/foundry/Kernel.test.sol +++ b/test/foundry/Kernel.test.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import "src/factory/KernelFactory.sol"; +import "src/factory/ECDSAKernelFactory.sol"; import "src/Kernel.sol"; import "src/validator/ECDSAValidator.sol"; import "src/factory/EIP1967Proxy.sol"; @@ -16,6 +17,7 @@ using ERC4337Utils for EntryPoint; contract KernelTest is Test { Kernel kernel; KernelFactory factory; + ECDSAKernelFactory ecdsaFactory; EntryPoint entryPoint; ECDSAValidator validator; address owner; @@ -26,9 +28,11 @@ contract KernelTest is Test { (owner, ownerKey) = makeAddrAndKey("owner"); entryPoint = new EntryPoint(); factory = new KernelFactory(entryPoint); + validator = new ECDSAValidator(); + ecdsaFactory = new ECDSAKernelFactory(factory, validator); - kernel = Kernel(payable(address(factory.createAccount(owner, 0)))); + kernel = Kernel(payable(address(ecdsaFactory.createAccount(owner, 0)))); vm.deal(address(kernel), 1e30); beneficiary = payable(address(makeAddr("beneficiary"))); } @@ -91,15 +95,17 @@ contract KernelTest is Test { } function test_set_execution() external { + TestValidator newValidator = new TestValidator(); UserOperation memory op = entryPoint.fillUserOp( address(kernel), abi.encodeWithSelector( KernelStorage.setExecution.selector, bytes4(0xdeadbeef), address(0xdead), - address(0xbeef), + address(newValidator), + uint48(0), uint48(0), - uint48(0) + bytes("") ) ); op.signature = abi.encodePacked(bytes4(0x00000000), entryPoint.signUserOpHash(vm, ownerKey, op)); @@ -108,7 +114,7 @@ contract KernelTest is Test { entryPoint.handleOps(ops, beneficiary); ExecutionDetail memory execution = KernelStorage(address(kernel)).getExecution(bytes4(0xdeadbeef)); assertEq(execution.executor, address(0xdead)); - assertEq(address(execution.validator), address(0xbeef)); + assertEq(address(execution.validator), address(newValidator)); assertEq(uint256(execution.validUntil), uint256(0)); assertEq(uint256(execution.validAfter), uint256(0)); } diff --git a/test/foundry/KernelExecution.test.sol b/test/foundry/KernelExecution.test.sol index d9a4d94a..7fdfb342 100644 --- a/test/foundry/KernelExecution.test.sol +++ b/test/foundry/KernelExecution.test.sol @@ -5,6 +5,7 @@ import "src/Kernel.sol"; import "src/validator/ECDSAValidator.sol"; import "src/factory/EIP1967Proxy.sol"; import "src/factory/KernelFactory.sol"; +import "src/factory/ECDSAKernelFactory.sol"; // test artifacts import "src/test/TestValidator.sol"; import "src/test/TestExecutor.sol"; @@ -21,6 +22,7 @@ using ERC4337Utils for EntryPoint; contract KernelExecutionTest is Test { Kernel kernel; KernelFactory factory; + ECDSAKernelFactory ecdsaFactory; EntryPoint entryPoint; ECDSAValidator validator; address owner; @@ -31,9 +33,11 @@ contract KernelExecutionTest is Test { (owner, ownerKey) = makeAddrAndKey("owner"); entryPoint = new EntryPoint(); factory = new KernelFactory(entryPoint); + validator = new ECDSAValidator(); + ecdsaFactory = new ECDSAKernelFactory(factory, validator); - kernel = Kernel(payable(address(factory.createAccount(owner, 0)))); + kernel = Kernel(payable(address(ecdsaFactory.createAccount(owner, 0)))); vm.deal(address(kernel), 1e30); beneficiary = payable(address(makeAddr("beneficiary"))); } From da524abd726927f8b69f5c2c98c6bdd07f5ca0b5 Mon Sep 17 00:00:00 2001 From: Derek Chiang Date: Sun, 21 May 2023 16:44:42 -0700 Subject: [PATCH 18/18] Update README --- README.md | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 27b911e6..44e8d248 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,6 @@ # Kernel -## Modular smart contract +Kernel is a minimal smart contract account designed to be extended. -Adding new feature will be same as adding a new facet for erc2535 diamond standard. - -For example, if you want to add a erc721 transfer feature, you can add a new facet for erc721 transfer feature. - -And all those features has it's own validation logic, which has to be done through `validateUserOp` function - -this validation logic can be set by the user, and it can be changed by user - -So essentially, there will be -1. validation module per function -2. diamond facet for implementing the function - -## Things to consider for implementing the validation module - -In Kernel, validation module is called with `call` not `delegatecall`, which means that the validation module can not change the state of the Kernel itself. - -But, this does comes with some limitation, **STORAGE ACCESS RULE**. Since erc4337 does not allow the userOp validation to access any storage outside of the account except the storage slot is related to the account address. So, if you are developing the Kernel validation module, you have to set the storage to not access any storage that violates the rule. \ No newline at end of file +- [Docs](https://docs.zerodev.app/extend-wallets/overview) +- [Code](https://github.com/zerodevapp/kernel) \ No newline at end of file