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/README.md b/README.md index 02acff35..c8544eaf 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ # Kernel -A minimal and efficient ERC-4337-compatible smart contract account designed to be extended. +Kernel is a minimal smart contract account designed to be extended. -- Intro blog: https://docs.zerodev.app/blog/kernel-minimal-extensible-account-for-aa-wallets -- Build plugins: https://docs.zerodev.app/use-wallets/use-session-keys -- Use ZeroDev: https://docs.zerodev.app/ +- [Docs](https://docs.zerodev.app/extend-wallets/overview) +- [Code](https://github.com/zerodevapp/kernel) ## Build diff --git a/lcov.info b/lcov.info deleted file mode 100644 index b5a7364a..00000000 --- a/lcov.info +++ /dev/null @@ -1,416 +0,0 @@ -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 -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,- -DA:93,0 -DA:94,0 -BRDA:94,8,0,- -BRDA:94,8,1,- -DA:96,0 -DA:97,0 -DA:98,0 -DA:99,0 -DA:100,0 -DA:102,0 -DA:111,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: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: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: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 -end_of_record -TN: -SF:src/abstract/Compatibility.sol -FN:8,Compatibility.onERC721Received -FNDA:0,Compatibility.onERC721Received -DA:14,0 -FN:17,Compatibility.onERC1155Received -FNDA:0,Compatibility.onERC1155Received -DA:24,0 -FN:27,Compatibility.onERC1155BatchReceived -FNDA:0,Compatibility.onERC1155BatchReceived -DA:34,0 -FNF:3 -FNH:0 -LF:3 -LH:0 -BRF:0 -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 -FNF:1 -FNH:1 -LF:2 -LH:2 -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 -FNF:2 -FNH:2 -LF:3 -LH:3 -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 -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 -LH:0 -BRF:0 -BRH:0 -end_of_record -TN: -SF:src/plugin/policy/SudoPolicy.sol -FN:6,SudoPolicy.executeAndRevert -FNDA:0,SudoPolicy.executeAndRevert -DA:12,0 -FNF:1 -FNH:0 -LF:1 -LH:0 -BRF:0 -BRH:0 -end_of_record -TN: -SF:src/utils/Exec.sol -FN:16,Exec.call -FNDA:0,Exec.call -DA:22,0 -DA:28,0 -FN:32,Exec.staticcall -FNDA:0,Exec.staticcall -DA:37,0 -DA:43,0 -FN:48,Exec.delegateCall -FNDA:0,Exec.delegateCall -DA:53,0 -DA:59,0 -FNF:3 -FNH:0 -LF:6 -LH:0 -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 -DA:15,0 -BRDA:15,0,0,- -DA:16,0 -DA:18,0 -BRDA:18,1,0,- -DA:19,0 -DA:21,0 -BRDA:21,2,0,- -DA:22,0 -FNF:1 -FNH:0 -LF:10 -LH:0 -BRF:3 -BRH:0 -end_of_record 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/IKernel.sol b/src/IKernel.sol new file mode 100644 index 00000000..0ac75871 --- /dev/null +++ b/src/IKernel.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +interface IKernel {} diff --git a/src/Kernel.sol b/src/Kernel.sol index 86662fc1..4c6c377a 100644 --- a/src/Kernel.sol +++ b/src/Kernel.sol @@ -1,66 +1,52 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +// Importing external libraries and contracts 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"; -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"; +import "./utils/KernelHelper.sol"; /// @title Kernel /// @author taek /// @notice wallet kernel for minimal wallet functionality -/// @dev supports only 1 owner, multiple plugins 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.1"; + 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 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 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 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; + address facet = getKernelStorage().execution[sig].executor; + 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 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) - function executeAndRevert(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" - ); + /// @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; bytes memory ret; if (operation == Operation.DelegateCall) { @@ -75,53 +61,55 @@ 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) { require(msg.sender == address(entryPoint), "account: not from entryPoint"); - if (userOp.signature.length == 65) { - 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; + // mode based signature + 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 use given validator + // mode == 0x00000002 enable validator + UserOperation memory op = userOp; + IKernelValidator validator; + bytes4 sig = bytes4(userOp.callData[0:4]); + if (mode == 0x00000000) { + // sudo mode (use default validator) + op = userOp; + op.signature = userOp.signature[4:]; + 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 = _packValidationData(!res, validUntil, validAfter); + 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); + op.signature = remainSig; } else { - revert InvalidSignatureLength(); + 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}(""); @@ -130,59 +118,59 @@ contract Kernel is IAccount, EIP712, Compatibility, KernelStorage { } } - function _validateUserOp(UserOperation calldata userOp, bytes32 userOpHash) + function _approveValidator(bytes4 sig, bytes calldata signature) internal - view - returns (uint256 validationData) + returns (uint256 validationData, bytes calldata enableData, bytes calldata validationSig) { - WalletKernelStorage storage ws = getKernelStorage(); - if (ws.owner == ECDSA.recover(userOpHash, userOp.signature)) { - return validationData; - } + 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,address executor,bytes enableData)"), + bytes4(sig), + uint256(bytes32(signature[4:36])), + address(bytes20(signature[36:56])), + keccak256(enableData) + ) + ) + ); - bytes32 hash = ECDSA.toEthSignedMessageHash(userOpHash); - address recovered = ECDSA.recover(hash, userOp.signature); - if (ws.owner != recovered) { - return SIG_VALIDATION_FAILED; - } + validationData = _intersectValidationData( + getKernelStorage().defaultValidator.validateSignature( + enableDigest, signature[120 + enableDataLength:120 + enableDataLength + enableSignatureLength] + ), + uint256(bytes32(signature[4:36])) & (uint256(type(uint96).max) << 160) + ); + 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); } - - /** - * 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)) - } + + /// @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); + if (data.validAfter > block.timestamp) { + return 0xffffffff; } - return ret; - } - - /// @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/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 ae4d3bb0..25e46034 100644 --- a/src/abstract/KernelStorage.sol +++ b/src/abstract/KernelStorage.sol @@ -1,37 +1,67 @@ // 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; // 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 { - address owner; + 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 == getKernelStorage().owner || msg.sender == address(this), + msg.sender == address(entryPoint) || msg.sender == address(this), "account: not from entrypoint or owner or self" ); _; } + /// @param _entryPoint The address of the EntryPoint contract + /// @dev Sets up the EntryPoint contract address constructor(IEntryPoint _entryPoint) { entryPoint = _entryPoint; - getKernelStorage().owner = address(1); + getKernelStorage().defaultValidator = IKernelValidator(address(1)); + } + + // 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"); + ws.defaultValidator = _defaultValidator; + emit DefaultValidatorChanged(address(0), address(_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 to get the wallet kernel storage function getKernelStorage() internal pure returns (WalletKernelStorage storage ws) { bytes32 storagePosition = bytes32(uint256(keccak256("zerodev.kernel")) - 1); assembly { @@ -39,10 +69,7 @@ contract KernelStorage { } } - function getOwner() external view returns (address) { - return getKernelStorage().owner; - } - + // Function to upgrade the contract to a new implementation function upgradeTo(address _newImplementation) external onlyFromEntryPointOrOwnerOrSelf { bytes32 slot = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; assembly { @@ -51,10 +78,7 @@ contract KernelStorage { emit Upgraded(_newImplementation); } - function transferOwnership(address _newOwner) external onlyFromEntryPointOrOwnerOrSelf { - getKernelStorage().owner = _newOwner; - } - + // Functions to get the nonce from the entry point function getNonce() public view virtual returns (uint256) { return entryPoint.getNonce(address(this), 0); } @@ -62,5 +86,69 @@ contract KernelStorage { function getNonce(uint192 key) public view virtual returns (uint256) { return entryPoint.getNonce(address(this), key); } + + + // query storage + function getDefaultValidator() public view returns (IKernelValidator) { + return getKernelStorage().defaultValidator; + } + + function getDisabledMode() public view returns (bytes4) { + 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]; + } + + /// @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, + IKernelValidator _validator, + uint48 _validUntil, + uint48 _validAfter, + bytes calldata _enableData + ) external onlyFromEntryPointOrOwnerOrSelf { + getKernelStorage().execution[_selector] = ExecutionDetail({ + executor: _executor, + validator: _validator, + validUntil: _validUntil, + validAfter: _validAfter + }); + _validator.enable(_enableData); + emit ExecutionChanged(_selector, _executor, address(_validator)); + } + + 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); + } + + /// @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); + } } - \ No newline at end of file diff --git a/src/actions/ERC721Actions.sol b/src/actions/ERC721Actions.sol new file mode 100644 index 00000000..bc6aaacf --- /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); + } +} 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/KernelFactory.sol b/src/factory/KernelFactory.sol similarity index 60% rename from src/KernelFactory.sol rename to src/factory/KernelFactory.sol index b74d3247..964847f4 100644 --- a/src/KernelFactory.sol +++ b/src/factory/KernelFactory.sol @@ -2,8 +2,9 @@ pragma solidity ^0.8.0; import "openzeppelin-contracts/contracts/utils/Create2.sol"; -import "./factory/EIP1967Proxy.sol"; -import "./Kernel.sol"; +import "./EIP1967Proxy.sol"; +import "src/Kernel.sol"; +import "src/validator/ECDSAValidator.sol"; contract KernelFactory { Kernel public immutable kernelTemplate; @@ -14,14 +15,14 @@ contract KernelFactory { kernelTemplate = new Kernel(_entryPoint); } - 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(Kernel.initialize, (_owner))) + abi.encode(address(kernelTemplate), abi.encodeCall(KernelStorage.initialize, (_validator, _data))) ) ) ); @@ -29,20 +30,19 @@ contract KernelFactory { return EIP1967Proxy(payable(addr)); } proxy = - new EIP1967Proxy{salt: salt}(address(kernelTemplate), abi.encodeWithSelector(Kernel.initialize.selector, _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(Kernel.initialize, (_owner))) + abi.encode(address(kernelTemplate), abi.encodeCall(KernelStorage.initialize, (_validator, _data)) ) ) - ); + )); } -} +} \ No newline at end of file diff --git a/src/plugin/IPlugin.sol b/src/plugin/IPlugin.sol deleted file mode 100644 index 7fdf5e4d..00000000 --- a/src/plugin/IPlugin.sol +++ /dev/null @@ -1,10 +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, uint256 missingAccountFunds) - external - returns (bool); -} 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/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/TestExecutor.sol b/src/test/TestExecutor.sol new file mode 100644 index 00000000..f720a674 --- /dev/null +++ b/src/test/TestExecutor.sol @@ -0,0 +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 new file mode 100644 index 00000000..a59d4cff --- /dev/null +++ b/src/test/TestValidator.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "src/validator/IValidator.sol"; +import "forge-std/console.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 userOpHash, uint256) external override returns (uint256) { + emit TestValidateUserOp(userOpHash); + return 0; + } + + function enable(bytes calldata data) external override { + emit TestEnable(data); + } + + function disable(bytes calldata data) external override { + emit TestDisable(data); + } +} diff --git a/src/utils/KernelHelper.sol b/src/utils/KernelHelper.sol new file mode 100644 index 00000000..ef6ff33d --- /dev/null +++ b/src/utils/KernelHelper.sol @@ -0,0 +1,16 @@ +// 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); + 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 new file mode 100644 index 00000000..d4d9110c --- /dev/null +++ b/src/validator/ECDSAValidator.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +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; +} + +contract ECDSAValidator is IKernelValidator { + event OwnerChanged(address indexed kernel, address indexed oldOwner, address indexed newOwner); + + mapping(address => ECDSAValidatorStorage) public ecdsaValidatorStorage; + + 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[msg.sender].owner; + ecdsaValidatorStorage[msg.sender].owner = owner; + emit OwnerChanged(msg.sender, 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; + } +} 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/src/validator/IValidator.sol b/src/validator/IValidator.sol new file mode 100644 index 00000000..5644db3b --- /dev/null +++ b/src/validator/IValidator.sol @@ -0,0 +1,21 @@ +// 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 diff --git a/src/validator/KillSwitchValidator.sol b/src/validator/KillSwitchValidator.sol new file mode 100644 index 00000000..2897b745 --- /dev/null +++ b/src/validator/KillSwitchValidator.sol @@ -0,0 +1,61 @@ +// 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 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) { + 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 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/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..4ab6b887 --- /dev/null +++ b/test/foundry/Kernel.test.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +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"; +// 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 kernel; + KernelFactory factory; + ECDSAKernelFactory ecdsaFactory; + EntryPoint entryPoint; + ECDSAValidator validator; + address owner; + uint256 ownerKey; + address payable beneficiary; + + function setUp() public { + (owner, ownerKey) = makeAddrAndKey("owner"); + entryPoint = new EntryPoint(); + factory = new KernelFactory(entryPoint); + + validator = new ECDSAValidator(); + ecdsaFactory = new ECDSAKernelFactory(factory, validator); + + kernel = Kernel(payable(address(ecdsaFactory.createAccount(owner, 0)))); + 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(factory.kernelTemplate()), + abi.encodeWithSelector( + KernelStorage.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(); + bytes memory empty; + UserOperation memory op = entryPoint.fillUserOp( + 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); + ops[0] = op; + entryPoint.handleOps(ops, beneficiary); + assertEq(address(KernelStorage(address(kernel)).getDefaultValidator()), address(newValidator)); + } + + function test_disable_mode() external { + bytes memory empty; + UserOperation memory op = entryPoint.fillUserOp( + 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); + ops[0] = op; + entryPoint.handleOps(ops, beneficiary); + assertEq(uint256(bytes32(KernelStorage(address(kernel)).getDisabledMode())), 1 << 224); + } + + 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(newValidator), + uint48(0), + uint48(0), + bytes("") + ) + ); + op.signature = abi.encodePacked(bytes4(0x00000000), entryPoint.signUserOpHash(vm, ownerKey, op)); + UserOperation[] memory ops = new UserOperation[](1); + ops[0] = op; + entryPoint.handleOps(ops, beneficiary); + ExecutionDetail memory execution = KernelStorage(address(kernel)).getExecution(bytes4(0xdeadbeef)); + assertEq(execution.executor, address(0xdead)); + 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 new file mode 100644 index 00000000..7fdfb342 --- /dev/null +++ b/test/foundry/KernelExecution.test.sol @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +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"; +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; + +contract KernelExecutionTest is Test { + Kernel kernel; + KernelFactory factory; + ECDSAKernelFactory ecdsaFactory; + EntryPoint entryPoint; + ECDSAValidator validator; + address owner; + uint256 ownerKey; + address payable beneficiary; + + function setUp() public { + (owner, ownerKey) = makeAddrAndKey("owner"); + entryPoint = new EntryPoint(); + factory = new KernelFactory(entryPoint); + + validator = new ECDSAValidator(); + ecdsaFactory = new ECDSAKernelFactory(factory, validator); + + kernel = Kernel(payable(address(ecdsaFactory.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(0), empty) + ); + 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_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(); + 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); + logGas(op); + + 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; + logGas(op); + 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); + erc721.mint(address(kernel), 1); + 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; + 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)); + } + + 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 +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))); +} 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()); - }) -})