From fae6fc87a8801c621074ef6698054bcf18b55faa Mon Sep 17 00:00:00 2001 From: leekt Date: Tue, 13 Jun 2023 01:34:38 +0900 Subject: [PATCH 1/5] v4.0.0-beta.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 408f70c8..59361532 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@zerodevapp/contracts", "description": "ZeroDev Account Abstraction (EIP 4337) contracts", "main": "./dist/index.js", - "version": "4.0.0-beta.9", + "version": "4.0.0-beta.10", "scripts": { "prepack": "./scripts/prepack-contracts-package.sh", "postpack": "./scripts/postpack-contracts-package.sh" From 29aea8b001ea77e1ba0e44dacd3ca659350b43ff Mon Sep 17 00:00:00 2001 From: leekt Date: Thu, 22 Jun 2023 03:09:19 +0900 Subject: [PATCH 2/5] v4.0.0-beta.11 --- .gitmodules | 3 +++ lib/solady | 1 + package.json | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) create mode 160000 lib/solady diff --git a/.gitmodules b/.gitmodules index ac2a4fe4..918f59a3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts branch = v4.8.2 +[submodule "lib/solady"] + path = lib/solady + url = https://github.com/vectorized/solady diff --git a/lib/solady b/lib/solady new file mode 160000 index 00000000..50cbe190 --- /dev/null +++ b/lib/solady @@ -0,0 +1 @@ +Subproject commit 50cbe1909e773b7e4ba76049c75a203e626d55ba diff --git a/package.json b/package.json index 59361532..6d94c6cb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@zerodevapp/contracts", "description": "ZeroDev Account Abstraction (EIP 4337) contracts", "main": "./dist/index.js", - "version": "4.0.0-beta.10", + "version": "4.0.0-beta.11", "scripts": { "prepack": "./scripts/prepack-contracts-package.sh", "postpack": "./scripts/postpack-contracts-package.sh" From 86847c37028b2cf36b53b39ec99bea3cff641fe5 Mon Sep 17 00:00:00 2001 From: leekt Date: Thu, 22 Jun 2023 03:14:44 +0900 Subject: [PATCH 3/5] v4.0.0-beta.12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6d94c6cb..fabe400a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@zerodevapp/contracts", "description": "ZeroDev Account Abstraction (EIP 4337) contracts", "main": "./dist/index.js", - "version": "4.0.0-beta.11", + "version": "4.0.0-beta.12", "scripts": { "prepack": "./scripts/prepack-contracts-package.sh", "postpack": "./scripts/postpack-contracts-package.sh" From 7c56e02ec7b34ab6923e696f865718f2d65a4031 Mon Sep 17 00:00:00 2001 From: leekt Date: Mon, 26 Jun 2023 21:56:24 +0900 Subject: [PATCH 4/5] changed killswitch validator to plugin --- src/executor/KillSwitchAction.sol | 25 ++++ src/validator/KillSwitchValidator.sol | 63 +++++---- test/foundry/KillSwitch.t.sol | 188 ++++++++++++++++++++++++++ 3 files changed, 249 insertions(+), 27 deletions(-) create mode 100644 src/executor/KillSwitchAction.sol create mode 100644 test/foundry/KillSwitch.t.sol diff --git a/src/executor/KillSwitchAction.sol b/src/executor/KillSwitchAction.sol new file mode 100644 index 00000000..6a0f455b --- /dev/null +++ b/src/executor/KillSwitchAction.sol @@ -0,0 +1,25 @@ +import "src/validator/IValidator.sol"; +import "src/abstract/KernelStorage.sol"; + +contract KillSwitchAction { + IKernelValidator public immutable killSwitchValidator; + + constructor(IKernelValidator _killswitchValidator) { + killSwitchValidator = _killswitchValidator; + } + + // Function to get the wallet kernel storage + function getKernelStorage() internal pure returns (WalletKernelStorage storage ws) { + bytes32 storagePosition = bytes32(uint256(keccak256("zerodev.kernel")) - 1); + assembly { + ws.slot := storagePosition + } + } + + function activateKillSwitch() external { + WalletKernelStorage storage ws = getKernelStorage(); + ws.defaultValidator = killSwitchValidator; + getKernelStorage().disabledMode = bytes4(0xffffffff); + getKernelStorage().lastDisabledTime = uint48(block.timestamp); + } +} diff --git a/src/validator/KillSwitchValidator.sol b/src/validator/KillSwitchValidator.sol index 3840e310..f72f3937 100644 --- a/src/validator/KillSwitchValidator.sol +++ b/src/validator/KillSwitchValidator.sol @@ -7,11 +7,13 @@ import "openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; import "src/utils/KernelHelper.sol"; import "account-abstraction/core/Helpers.sol"; import "src/Kernel.sol"; +import { WalletKernelStorage, ExecutionDetail} from "src/abstract/KernelStorage.sol"; import "./ECDSAValidator.sol"; + struct KillSwitchValidatorStorage { - address owner; address guardian; + IKernelValidator validator; uint48 pausedUntil; } @@ -19,8 +21,7 @@ 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])); + killSwitchValidatorStorage[msg.sender].guardian = address(bytes20(enableData[0:20])); } function disable(bytes calldata) external override { @@ -29,9 +30,13 @@ contract KillSwitchValidator is IKernelValidator { 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 - ); + uint256 res = validatorStorage.validator.validateSignature(hash,signature); + uint48 pausedUntil = validatorStorage.pausedUntil; + ValidationData memory validationData = _parseValidationData(res); + if(validationData.aggregator != address(1)) { // if signature verification has not been failed, return with the result + uint256 delayedData = _packValidationData(false, 0, pausedUntil); + return _packValidationData(_intersectTimeRange(res, delayedData)); + } } function validateUserOp(UserOperation calldata _userOp, bytes32 _userOpHash, uint256) @@ -39,30 +44,34 @@ contract KillSwitchValidator is IKernelValidator { override returns (uint256) { - address signer; - bytes calldata signature; KillSwitchValidatorStorage storage validatorStorage = killSwitchValidatorStorage[_userOp.sender]; - if (_userOp.signature.length == 6 + 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; + uint48 pausedUntil = validatorStorage.pausedUntil; + uint256 validationResult = 0; + if(address(validatorStorage.validator) != address(0)){ + // check for validator at first + try validatorStorage.validator.validateUserOp(_userOp, _userOpHash, pausedUntil) returns (uint256 res) { + validationResult = res; + } catch { + validationResult = SIG_VALIDATION_FAILED; + } + ValidationData memory validationData = _parseValidationData(validationResult); + if(validationData.aggregator != address(1)) { // if signature verification has not been failed, return with the result + uint256 delayedData = _packValidationData(false, 0, pausedUntil); + return _packValidationData(_intersectTimeRange(validationResult, delayedData)); + } } - 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) { + if(_userOp.signature.length == 71) { + // save data to this storage + validatorStorage.pausedUntil = uint48(bytes6(_userOp.signature[0:6])); + validatorStorage.validator = KernelStorage(msg.sender).getDefaultValidator(); + bytes32 hash = ECDSA.toEthSignedMessageHash(keccak256(bytes.concat(_userOp.signature[0:6],_userOpHash))); + address recovered = ECDSA.recover(hash, _userOp.signature[6:]); + if (validatorStorage.guardian != recovered) { + return SIG_VALIDATION_FAILED; + } + return _packValidationData(false, 0, pausedUntil); + } else { return SIG_VALIDATION_FAILED; } - return _packValidationData(false, 0, validatorStorage.pausedUntil); } } diff --git a/test/foundry/KillSwitch.t.sol b/test/foundry/KillSwitch.t.sol new file mode 100644 index 00000000..d285afce --- /dev/null +++ b/test/foundry/KillSwitch.t.sol @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "src/factory/KernelFactory.sol"; +import "src/factory/TempKernel.sol"; +import "src/validator/ECDSAValidator.sol"; +import "src/factory/ECDSAKernelFactory.sol"; +import "src/Kernel.sol"; +import "src/validator/KillSwitchValidator.sol"; +import "src/executor/KillSwitchAction.sol"; +import "src/factory/EIP1967Proxy.sol"; +// test utils +import "forge-std/Test.sol"; +import {ERC4337Utils} from "./ERC4337Utils.sol"; + +using ERC4337Utils for EntryPoint; + +contract KernelExecutionTest is Test { + Kernel kernel; + KernelFactory factory; + ECDSAKernelFactory ecdsaFactory; + EntryPoint entryPoint; + ECDSAValidator validator; + + KillSwitchValidator killSwitch; + KillSwitchAction action; + 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, entryPoint); + + kernel = Kernel(payable(address(ecdsaFactory.createAccount(owner, 0)))); + vm.deal(address(kernel), 1e30); + beneficiary = payable(address(makeAddr("beneficiary"))); + killSwitch = new KillSwitchValidator(); + action = new KillSwitchAction(killSwitch); + } + + function test_mode_2() external { + UserOperation memory op = entryPoint.fillUserOp( + address(kernel), + abi.encodeWithSelector(Kernel.execute.selector, owner, 0, "", Operation.Call) + ); + + op.signature = bytes.concat(bytes4(0), entryPoint.signUserOpHash(vm, ownerKey, op)); + UserOperation[] memory ops = new UserOperation[](1); + ops[0] = op; + entryPoint.handleOps(ops, beneficiary); + + + op = entryPoint.fillUserOp( + address(kernel), + abi.encodeWithSelector(KillSwitchAction.activateKillSwitch.selector) + ); + address guardianKeyAddr; + uint256 guardianKeyPriv; + (guardianKeyAddr, guardianKeyPriv) = makeAddrAndKey("guardianKey"); + bytes memory enableData = abi.encodePacked( + guardianKeyAddr + ); + { + bytes32 digest = getTypedDataHash( + address(kernel), + KillSwitchAction.activateKillSwitch.selector, + 0, + 0, + address(killSwitch), + address(action), + enableData + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey, digest); + + op.signature = abi.encodePacked( + bytes4(0x00000002), + uint48(0), + uint48(0), + address(killSwitch), + address(action), + uint256(enableData.length), + enableData, + uint256(65), + r, + s, + v + ); + } + + uint256 pausedUntil = block.timestamp + 1000; + + bytes32 hash = entryPoint.getUserOpHash(op); + { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(guardianKeyPriv, ECDSA.toEthSignedMessageHash(keccak256(bytes.concat(bytes6(uint48(pausedUntil)),hash)))); + bytes memory sig = abi.encodePacked(r, s, v); + + op.signature = bytes.concat(op.signature, bytes6(uint48(pausedUntil)), sig); + } + + ops[0] = op; + logGas(op); + entryPoint.handleOps(ops, beneficiary); + assertEq(address(kernel.getDefaultValidator()), address(killSwitch)); + op = entryPoint.fillUserOp( + address(kernel), + abi.encodeWithSelector(Kernel.execute.selector, owner, 0, "", Operation.Call) + ); + + op.signature = bytes.concat(bytes4(0), entryPoint.signUserOpHash(vm, ownerKey, op)); + ops[0] = op; + vm.expectRevert(); + entryPoint.handleOps(ops, beneficiary); // should revert because kill switch is active + vm.warp(pausedUntil + 1); + entryPoint.handleOps(ops, beneficiary); // should not revert because pausedUntil has been passed + } + + 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))); +} From 05c317f32fd2e56b88c8a51beaece7ba5fa6a676 Mon Sep 17 00:00:00 2001 From: leekt Date: Mon, 26 Jun 2023 21:58:05 +0900 Subject: [PATCH 5/5] v4.0.0-beta.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fabe400a..fe428eb5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@zerodevapp/contracts", "description": "ZeroDev Account Abstraction (EIP 4337) contracts", "main": "./dist/index.js", - "version": "4.0.0-beta.12", + "version": "4.0.0-beta.13", "scripts": { "prepack": "./scripts/prepack-contracts-package.sh", "postpack": "./scripts/postpack-contracts-package.sh"