diff --git a/src/validator/KillSwitchValidator.sol b/src/validator/KillSwitchValidator.sol index 721c0a65..5b7245ee 100644 --- a/src/validator/KillSwitchValidator.sol +++ b/src/validator/KillSwitchValidator.sol @@ -8,6 +8,7 @@ import "src/Kernel.sol"; import {WalletKernelStorage, ExecutionDetail} from "src/abstract/KernelStorage.sol"; import "src/interfaces/IValidator.sol"; import "src/common/Types.sol"; +import {KillSwitchAction} from "src/executor/KillSwitchAction.sol"; struct KillSwitchValidatorStorage { address guardian; @@ -66,6 +67,12 @@ contract KillSwitchValidator is IKernelValidator { // if signature verification has not been failed, return with the result ValidationData delayedData = packValidationData(pausedUntil, ValidUntil.wrap(0)); return _intersectValidationData(validationData, delayedData); + } else if (bytes4(_userOp.callData[0:4]) == KillSwitchAction.toggleKillSwitch.selector) { + bytes32 hash = ECDSA.toEthSignedMessageHash(_userOpHash); + address recovered = ECDSA.recover(hash, _userOp.signature); + if (validatorStorage.guardian == recovered) { + return packValidationData(ValidAfter.wrap(0), ValidUntil.wrap(0)); + } } } if (_userOp.signature.length == 71) { diff --git a/test/foundry/validator/KillSwitchValidator.t.sol b/test/foundry/validator/KillSwitchValidator.t.sol new file mode 100644 index 00000000..4102e189 --- /dev/null +++ b/test/foundry/validator/KillSwitchValidator.t.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "src/factory/KernelFactory.sol"; +import "src/validator/ECDSAValidator.sol"; +import "src/Kernel.sol"; +import "src/validator/KillSwitchValidator.sol"; +import "src/executor/KillSwitchAction.sol"; +// test utils +import "forge-std/Test.sol"; +import "test/foundry/utils/ERC4337Utils.sol"; + +using ERC4337Utils for EntryPoint; + +contract KillSwitchValidatorTest is KernelTestBase { + KillSwitchValidator killSwitch; + KillSwitchAction action; + address guardian; + uint256 guardianKey; + + function setUp() public { + _initialize(); + defaultValidator = new ECDSAValidator(); + _setAddress(); + (guardian, guardianKey) = makeAddrAndKey("guardian"); + killSwitch = new KillSwitchValidator(); + action = new KillSwitchAction(killSwitch); + } + + function test_force_unblock() 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.toggleKillSwitch.selector)); + address guardianKeyAddr; + uint256 guardianKeyPriv; + (guardianKeyAddr, guardianKeyPriv) = makeAddrAndKey("guardianKey"); + bytes memory enableData = abi.encodePacked(guardianKeyAddr); + { + bytes32 digest = getTypedDataHash( + address(kernel), + KillSwitchAction.toggleKillSwitch.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(kernel.getDisabledMode(), bytes4(0xffffffff)); + assertEq(address(kernel.getDefaultValidator()), address(killSwitch)); + op = entryPoint.fillUserOp(address(kernel), abi.encodeWithSelector(KillSwitchAction.toggleKillSwitch.selector)); + + op.signature = bytes.concat(bytes4(0), entryPoint.signUserOpHash(vm, guardianKeyPriv, op)); + ops[0] = op; + entryPoint.handleOps(ops, beneficiary); + assertEq(kernel.getDisabledMode(), bytes4(0)); + } + + 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.toggleKillSwitch.selector)); + address guardianKeyAddr; + uint256 guardianKeyPriv; + (guardianKeyAddr, guardianKeyPriv) = makeAddrAndKey("guardianKey"); + bytes memory enableData = abi.encodePacked(guardianKeyAddr); + { + bytes32 digest = getTypedDataHash( + address(kernel), + KillSwitchAction.toggleKillSwitch.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 + } +}