diff --git a/src/account/PluginManager2.sol b/src/account/PluginManager2.sol index 2f60752e..7898f5d0 100644 --- a/src/account/PluginManager2.sol +++ b/src/account/PluginManager2.sol @@ -7,6 +7,7 @@ import {IPlugin} from "../interfaces/IPlugin.sol"; import {FunctionReference} from "../interfaces/IPluginManager.sol"; import {FunctionReferenceLib} from "../helpers/FunctionReferenceLib.sol"; import {AccountStorage, getAccountStorage, toSetValue, toFunctionReference} from "./AccountStorage.sol"; +import {ExecutionHook} from "../interfaces/IAccountLoupe.sol"; // Temporary additional functions for a user-controlled install flow for validation functions. abstract contract PluginManager2 { @@ -16,13 +17,15 @@ abstract contract PluginManager2 { error ValidationAlreadySet(bytes4 selector, FunctionReference validationFunction); error PreValidationAlreadySet(FunctionReference validationFunction, FunctionReference preValidationFunction); error ValidationNotSet(bytes4 selector, FunctionReference validationFunction); + error PermissionAlreadySet(FunctionReference validationFunction, ExecutionHook hook); function _installValidation( FunctionReference validationFunction, bool isDefault, bytes4[] memory selectors, bytes calldata installData, - bytes memory preValidationHooks + bytes memory preValidationHooks, + bytes memory permissionHooks ) // TODO: flag for signature validation internal @@ -51,6 +54,26 @@ abstract contract PluginManager2 { } } + if (permissionHooks.length > 0) { + (ExecutionHook[] memory permissionFunctions, bytes[] memory initDatas) = + abi.decode(permissionHooks, (ExecutionHook[], bytes[])); + + for (uint256 i = 0; i < permissionFunctions.length; ++i) { + ExecutionHook memory permissionFunction = permissionFunctions[i]; + + if ( + !_storage.validationData[validationFunction].permissionHooks.add(toSetValue(permissionFunction)) + ) { + revert PermissionAlreadySet(validationFunction, permissionFunction); + } + + if (initDatas[i].length > 0) { + (address executionPlugin,) = FunctionReferenceLib.unpack(permissionFunction.hookFunction); + IPlugin(executionPlugin).onInstall(initDatas[i]); + } + } + } + if (isDefault) { if (_storage.validationData[validationFunction].isDefault) { revert DefaultValidationAlreadySet(validationFunction); @@ -75,27 +98,45 @@ abstract contract PluginManager2 { FunctionReference validationFunction, bytes4[] calldata selectors, bytes calldata uninstallData, - bytes calldata preValidationHookUninstallData + bytes calldata preValidationHookUninstallData, + bytes calldata permissionHookUninstallData ) internal { AccountStorage storage _storage = getAccountStorage(); _storage.validationData[validationFunction].isDefault = false; _storage.validationData[validationFunction].isSignatureValidation = false; - bytes[] memory preValidationHookUninstallDatas = abi.decode(preValidationHookUninstallData, (bytes[])); - - // Clear pre validation hooks - EnumerableSet.Bytes32Set storage preValidationHooks = - _storage.validationData[validationFunction].preValidationHooks; - while (preValidationHooks.length() > 0) { - FunctionReference preValidationFunction = toFunctionReference(preValidationHooks.at(0)); - preValidationHooks.remove(toSetValue(preValidationFunction)); - (address preValidationPlugin,) = FunctionReferenceLib.unpack(preValidationFunction); - if (preValidationHookUninstallDatas[0].length > 0) { - IPlugin(preValidationPlugin).onUninstall(preValidationHookUninstallDatas[0]); + { + bytes[] memory preValidationHookUninstallDatas = abi.decode(preValidationHookUninstallData, (bytes[])); + + // Clear pre validation hooks + EnumerableSet.Bytes32Set storage preValidationHooks = + _storage.validationData[validationFunction].preValidationHooks; + while (preValidationHooks.length() > 0) { + FunctionReference preValidationFunction = toFunctionReference(preValidationHooks.at(0)); + preValidationHooks.remove(toSetValue(preValidationFunction)); + (address preValidationPlugin,) = FunctionReferenceLib.unpack(preValidationFunction); + if (preValidationHookUninstallDatas[0].length > 0) { + IPlugin(preValidationPlugin).onUninstall(preValidationHookUninstallDatas[0]); + } } } + { + bytes[] memory permissionHookUninstallDatas = abi.decode(permissionHookUninstallData, (bytes[])); + + // Clear permission hooks + EnumerableSet.Bytes32Set storage permissionHooks = + _storage.validationData[validationFunction].permissionHooks; + while (permissionHooks.length() > 0) { + FunctionReference permissionHook = toFunctionReference(permissionHooks.at(0)); + permissionHooks.remove(toSetValue(permissionHook)); + (address permissionHookPlugin,) = FunctionReferenceLib.unpack(permissionHook); + if (permissionHookUninstallDatas[0].length > 0) { + IPlugin(permissionHookPlugin).onUninstall(permissionHookUninstallDatas[0]); + } + } + } // Because this function also calls `onUninstall`, and removes the shared flag from validation, we must // assume these selectors passed in to be exhaustive. // TODO: consider enforcing this from user-supplied install config. diff --git a/src/account/UpgradeableModularAccount.sol b/src/account/UpgradeableModularAccount.sol index 9c5e4173..985a8ee0 100644 --- a/src/account/UpgradeableModularAccount.sol +++ b/src/account/UpgradeableModularAccount.sol @@ -291,8 +291,11 @@ contract UpgradeableModularAccount is _doRuntimeValidation(runtimeValidationFunction, data, authorization[22:]); - // If runtime validation passes, execute the call + // If runtime validation passes, do runtime permission checks + PostExecToRun[] memory postPermissionHooks = + _doPreHooks(getAccountStorage().validationData[runtimeValidationFunction].permissionHooks, data, false); + // Execute the call (bool success, bytes memory returnData) = address(this).call(data); if (!success) { @@ -301,6 +304,8 @@ contract UpgradeableModularAccount is } } + _doCachedPostExecHooks(postPermissionHooks); + return returnData; } @@ -342,7 +347,7 @@ contract UpgradeableModularAccount is external initializer { - _installValidation(validationFunction, true, new bytes4[](0), installData, bytes("")); + _installValidation(validationFunction, true, new bytes4[](0), installData, bytes(""), bytes("")); emit ModularAccountInitialized(_ENTRY_POINT); } @@ -353,9 +358,12 @@ contract UpgradeableModularAccount is bool isDefault, bytes4[] memory selectors, bytes calldata installData, - bytes calldata preValidationHooks + bytes calldata preValidationHooks, + bytes calldata permissionHooks ) external wrapNativeFunction { - _installValidation(validationFunction, isDefault, selectors, installData, preValidationHooks); + _installValidation( + validationFunction, isDefault, selectors, installData, preValidationHooks, permissionHooks + ); } /// @inheritdoc IPluginManager @@ -364,9 +372,16 @@ contract UpgradeableModularAccount is FunctionReference validationFunction, bytes4[] calldata selectors, bytes calldata uninstallData, - bytes calldata preValidationHookUninstallData + bytes calldata preValidationHookUninstallData, + bytes calldata permissionHookUninstallData ) external wrapNativeFunction { - _uninstallValidation(validationFunction, selectors, uninstallData, preValidationHookUninstallData); + _uninstallValidation( + validationFunction, + selectors, + uninstallData, + preValidationHookUninstallData, + permissionHookUninstallData + ); } /// @notice ERC165 introspection @@ -434,6 +449,9 @@ contract UpgradeableModularAccount is revert UnrecognizedFunction(bytes4(userOp.callData)); } bytes4 selector = bytes4(userOp.callData); + if (selector == this.executeUserOp.selector) { + selector = bytes4(userOp.callData[4:8]); + } // Revert if the provided `authorization` less than 21 bytes long, rather than right-padding. FunctionReference userOpValidationFunction = FunctionReference.wrap(bytes21(userOp.signature[:21])); @@ -604,6 +622,7 @@ contract UpgradeableModularAccount is try IExecutionHook(plugin).preExecutionHook(functionId, data) returns (bytes memory returnData) { preExecHookReturnData = returnData; } catch (bytes memory revertReason) { + // TODO: same issue with EP0.6 - we can't do bytes4 error codes revert PreExecHookReverted(plugin, functionId, revertReason); } } diff --git a/src/interfaces/IPluginManager.sol b/src/interfaces/IPluginManager.sol index 717e1fa0..32634e34 100644 --- a/src/interfaces/IPluginManager.sol +++ b/src/interfaces/IPluginManager.sol @@ -32,12 +32,15 @@ interface IPluginManager { /// @param isDefault Whether the validation function applies for all selectors in the default pool. /// @param selectors The selectors to install the validation function for. /// @param installData Optional data to be decoded and used by the plugin to setup initial plugin state. + /// @param preValidationHooks Optional pre-validation hooks to install for the validation function. + /// @param permissionHooks Optional permission hooks to install for the validation function. function installValidation( FunctionReference validationFunction, bool isDefault, bytes4[] memory selectors, bytes calldata installData, - bytes calldata preValidationHooks + bytes calldata preValidationHooks, + bytes calldata permissionHooks ) external; /// @notice Uninstall a validation function from a set of execution selectors. @@ -46,11 +49,15 @@ interface IPluginManager { /// @param selectors The selectors to uninstall the validation function for. /// @param uninstallData Optional data to be decoded and used by the plugin to clear plugin data for the /// account. + /// @param preValidationHookUninstallData Optional data to be decoded and used by the plugin to clear account + /// data + /// @param permissionHookUninstallData Optional data to be decoded and used by the plugin to clear account data function uninstallValidation( FunctionReference validationFunction, bytes4[] calldata selectors, bytes calldata uninstallData, - bytes calldata preValidationHookUninstallData + bytes calldata preValidationHookUninstallData, + bytes calldata permissionHookUninstallData ) external; /// @notice Uninstall a plugin from the modular account. diff --git a/src/plugins/NativeTokenLimitPlugin.sol b/src/plugins/NativeTokenLimitPlugin.sol new file mode 100644 index 00000000..5127f18a --- /dev/null +++ b/src/plugins/NativeTokenLimitPlugin.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.25; + +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; +import {UserOperationLib} from "@eth-infinitism/account-abstraction/core/UserOperationLib.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {IAccountExecute} from "@eth-infinitism/account-abstraction/interfaces/IAccountExecute.sol"; + +import {PluginManifest, PluginMetadata} from "../interfaces/IPlugin.sol"; +import {IStandardExecutor, Call} from "../interfaces/IStandardExecutor.sol"; +import {IPlugin} from "../interfaces/IPlugin.sol"; +import {IExecutionHook} from "../interfaces/IExecutionHook.sol"; +import {IValidationHook} from "../interfaces/IValidationHook.sol"; +import {BasePlugin, IERC165} from "./BasePlugin.sol"; + +/// @title Native Token Limit Plugin +/// @author ERC-6900 Authors +/// @notice This plugin supports a single total native token spend limit. +/// It tracks a total spend limit across UserOperation gas limits and native token transfers. +/// If a paymaster is used, UO gas would not cause the limit to decrease. +contract NativeTokenLimitPlugin is BasePlugin, IExecutionHook, IValidationHook { + using UserOperationLib for PackedUserOperation; + using EnumerableSet for EnumerableSet.Bytes32Set; + + string public constant NAME = "Native Token Limit"; + string public constant VERSION = "1.0.0"; + string public constant AUTHOR = "ERC-6900 Authors"; + + mapping(address account => uint256[] limits) public limits; + + error ExceededNativeTokenLimit(); + error ExceededNumberOfEntities(); + + function updateLimits(uint8 functionId, uint256 newLimit) external { + limits[msg.sender][functionId] = newLimit; + } + + /// @inheritdoc IExecutionHook + function preExecutionHook(uint8 functionId, bytes calldata data) external override returns (bytes memory) { + bytes calldata callData; + bytes4 execSelector; + + // TODO: plugins should never have to do these gymnastics + execSelector = bytes4(data[52:56]); + if (execSelector == IAccountExecute.executeUserOp.selector) { + callData = data[56:]; + execSelector = bytes4(callData); + } else { + callData = data[52:]; + } + + uint256 value; + // Get value being sent + if (execSelector == IStandardExecutor.execute.selector) { + value = uint256(bytes32(callData[36:68])); + } else if (execSelector == IStandardExecutor.executeBatch.selector) { + Call[] memory calls = abi.decode(callData[4:], (Call[])); + for (uint256 i = 0; i < calls.length; i++) { + value += calls[i].value; + } + } + + uint256 limit = limits[msg.sender][functionId]; + if (value > limit) { + revert ExceededNativeTokenLimit(); + } + limits[msg.sender][functionId] = limit - value; + + return ""; + } + + /// @inheritdoc IExecutionHook + function postExecutionHook(uint8, bytes calldata) external pure override { + revert NotImplemented(); + } + + // No implementation, no revert + // Runtime spends no account gas, and we check native token spend limits in exec hooks + function preRuntimeValidationHook(uint8 functionId, address, uint256, bytes calldata) external pure override { + // silence warnings + (functionId); + } + + /// @inheritdoc IValidationHook + function preUserOpValidationHook(uint8 functionId, PackedUserOperation calldata userOp, bytes32) + external + returns (uint256) + { + // Decrease limit only if no paymaster is used + if (userOp.paymasterAndData.length == 0) { + uint256 vgl = UserOperationLib.unpackVerificationGasLimit(userOp); + uint256 cgl = UserOperationLib.unpackCallGasLimit(userOp); + uint256 totalGas = userOp.preVerificationGas + vgl + cgl; + uint256 usage = totalGas * UserOperationLib.unpackMaxFeePerGas(userOp); + + uint256 limit = limits[msg.sender][functionId]; + if (usage > limit) { + revert ExceededNativeTokenLimit(); + } + limits[msg.sender][functionId] = limit - usage; + } + return 0; + } + + /// @inheritdoc IPlugin + function onInstall(bytes calldata data) external override { + uint256[] memory spendLimits = abi.decode(data, (uint256[])); + + for (uint256 i = 0; i < spendLimits.length; i++) { + limits[msg.sender].push(spendLimits[i]); + } + + if (limits[msg.sender].length > type(uint8).max) { + revert ExceededNumberOfEntities(); + } + } + + /// @inheritdoc IPlugin + function onUninstall(bytes calldata data) external override { + uint8 functionId = abi.decode(data, (uint8)); + delete limits[msg.sender][functionId]; + } + + /// @inheritdoc IPlugin + function pluginManifest() external pure override returns (PluginManifest memory) { + // silence warnings + PluginManifest memory manifest; + return manifest; + } + + /// @inheritdoc IPlugin + function pluginMetadata() external pure virtual override returns (PluginMetadata memory) { + PluginMetadata memory metadata; + metadata.name = NAME; + metadata.version = VERSION; + metadata.author = AUTHOR; + + metadata.permissionRequest = new string[](2); + metadata.permissionRequest[0] = "native-token-limit"; + metadata.permissionRequest[1] = "gas-limit"; + return metadata; + } + + // ┏━━━━━━━━━━━━━━━┓ + // ┃ EIP-165 ┃ + // ┗━━━━━━━━━━━━━━━┛ + + /// @inheritdoc BasePlugin + function supportsInterface(bytes4 interfaceId) public view override(BasePlugin, IERC165) returns (bool) { + return super.supportsInterface(interfaceId); + } +} diff --git a/test/account/AccountLoupe.t.sol b/test/account/AccountLoupe.t.sol index e21f232f..54f9d10e 100644 --- a/test/account/AccountLoupe.t.sol +++ b/test/account/AccountLoupe.t.sol @@ -43,7 +43,7 @@ contract AccountLoupeTest is AccountTestBase { bytes[] memory installDatas = new bytes[](2); vm.prank(address(entryPoint)); account1.installValidation( - ownerValidation, true, new bytes4[](0), bytes(""), abi.encode(preValidationHooks, installDatas) + ownerValidation, true, new bytes4[](0), bytes(""), abi.encode(preValidationHooks, installDatas), "" ); } diff --git a/test/account/AccountPermissionHooks.t.sol b/test/account/AccountPermissionHooks.t.sol index a82f4b13..5fa98340 100644 --- a/test/account/AccountPermissionHooks.t.sol +++ b/test/account/AccountPermissionHooks.t.sol @@ -213,7 +213,7 @@ contract AccountPermissionHooksTest is AccountTestBase { ManifestExecutionFunction({ executionSelector: _EXEC_SELECTOR, isPublic: false, - allowSharedValidation: false + allowDefaultValidation: false }) ); _m1.validationFunctions.push(validation); diff --git a/test/account/ValidationIntersection.t.sol b/test/account/ValidationIntersection.t.sol index 877f8ff9..a90dc91e 100644 --- a/test/account/ValidationIntersection.t.sol +++ b/test/account/ValidationIntersection.t.sol @@ -67,7 +67,7 @@ contract ValidationIntersectionTest is AccountTestBase { }); bytes[] memory installDatas = new bytes[](1); account1.installValidation( - oneHookValidation, true, new bytes4[](0), bytes(""), abi.encode(preValidationHooks, installDatas) + oneHookValidation, true, new bytes4[](0), bytes(""), abi.encode(preValidationHooks, installDatas), "" ); account1.installPlugin({ plugin: address(twoHookPlugin), @@ -87,7 +87,7 @@ contract ValidationIntersectionTest is AccountTestBase { }); installDatas = new bytes[](2); account1.installValidation( - twoHookValidation, true, new bytes4[](0), bytes(""), abi.encode(preValidationHooks, installDatas) + twoHookValidation, true, new bytes4[](0), bytes(""), abi.encode(preValidationHooks, installDatas), "" ); vm.stopPrank(); } diff --git a/test/plugin/NativeTokenLimitPlugin.t.sol b/test/plugin/NativeTokenLimitPlugin.t.sol new file mode 100644 index 00000000..4284d36e --- /dev/null +++ b/test/plugin/NativeTokenLimitPlugin.t.sol @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {EntryPoint} from "@eth-infinitism/account-abstraction/core/EntryPoint.sol"; +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; + +import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; +import {FunctionReference} from "../../src/helpers/FunctionReferenceLib.sol"; +import {NativeTokenLimitPlugin} from "../../src/plugins/NativeTokenLimitPlugin.sol"; +import {MockPlugin} from "../mocks/MockPlugin.sol"; +import {ExecutionHook} from "../../src/interfaces/IAccountLoupe.sol"; +import {FunctionReferenceLib} from "../../src/helpers/FunctionReferenceLib.sol"; +import {IStandardExecutor, Call} from "../../src/interfaces/IStandardExecutor.sol"; +import {PluginManifest} from "../../src/interfaces/IPlugin.sol"; + +import {MSCAFactoryFixture} from "../mocks/MSCAFactoryFixture.sol"; +import {OptimizedTest} from "../utils/OptimizedTest.sol"; + +contract NativeTokenLimitPluginTest is OptimizedTest { + EntryPoint public entryPoint = new EntryPoint(); + address public recipient = address(1); + address payable public bundler = payable(address(2)); + PluginManifest internal _m; + MockPlugin public validationPlugin = new MockPlugin(_m); + FunctionReference public validationFunction; + + UpgradeableModularAccount public acct; + NativeTokenLimitPlugin public plugin = new NativeTokenLimitPlugin(); + uint256 public spendLimit = 10 ether; + + function setUp() public { + // Set up a validator with hooks from the gas spend limit plugin attached + + MSCAFactoryFixture factory = new MSCAFactoryFixture(entryPoint, _deploySingleOwnerPlugin()); + + acct = factory.createAccount(address(this), 0); + + vm.deal(address(acct), 10 ether); + + FunctionReference[] memory preValidationHooks = new FunctionReference[](1); + preValidationHooks[0] = FunctionReferenceLib.pack(address(plugin), 0); + + ExecutionHook[] memory permissionHooks = new ExecutionHook[](1); + permissionHooks[0] = ExecutionHook({ + hookFunction: FunctionReferenceLib.pack(address(plugin), 0), + isPreHook: true, + isPostHook: false, + requireUOContext: false + }); + + uint256[] memory spendLimits = new uint256[](1); + spendLimits[0] = spendLimit; + + bytes[] memory preValHooksInitDatas = new bytes[](1); + preValHooksInitDatas[0] = ""; + + bytes[] memory permissionInitDatas = new bytes[](1); + permissionInitDatas[0] = abi.encode(spendLimits); + + vm.prank(address(acct)); + acct.installValidation( + FunctionReferenceLib.pack(address(validationPlugin), 0), + true, + new bytes4[](0), + new bytes(0), + abi.encode(preValidationHooks, preValHooksInitDatas), + abi.encode(permissionHooks, permissionInitDatas) + ); + + validationFunction = FunctionReferenceLib.pack(address(validationPlugin), 0); + } + + function _getExecuteWithValue(uint256 value) internal view returns (bytes memory) { + return abi.encodeCall(UpgradeableModularAccount.execute, (recipient, value, "")); + } + + function _getPackedUO(uint256 gas1, uint256 gas2, uint256 gas3, uint256 gasPrice, bytes memory callData) + internal + view + returns (PackedUserOperation memory uo) + { + uo = PackedUserOperation({ + sender: address(acct), + nonce: 0, + initCode: "", + callData: abi.encodePacked(UpgradeableModularAccount.executeUserOp.selector, callData), + accountGasLimits: bytes32(bytes16(uint128(gas1))) | bytes32(uint256(gas2)), + preVerificationGas: gas3, + gasFees: bytes32(uint256(uint128(gasPrice))), + paymasterAndData: "", + signature: abi.encodePacked(FunctionReferenceLib.pack(address(validationPlugin), 0), uint8(1)) + }); + } + + function test_userOp_gasLimit() public { + vm.startPrank(address(entryPoint)); + + // uses 10e - 200000 of gas + assertEq(plugin.limits(address(acct), 0), 10 ether); + uint256 result = acct.validateUserOp( + _getPackedUO(100000, 100000, 10 ether - 400000, 1, _getExecuteWithValue(0)), bytes32(0), 0 + ); + assertEq(plugin.limits(address(acct), 0), 200000); + + uint256 expected = uint256(type(uint48).max) << 160; + assertEq(result, expected); + + // uses 200k + 1 wei of gas + vm.expectRevert(NativeTokenLimitPlugin.ExceededNativeTokenLimit.selector); + result = acct.validateUserOp(_getPackedUO(100000, 100000, 1, 1, _getExecuteWithValue(0)), bytes32(0), 0); + } + + function test_userOp_executeLimit() public { + vm.startPrank(address(entryPoint)); + + // uses 5e of native tokens + assertEq(plugin.limits(address(acct), 0), 10 ether); + acct.executeUserOp(_getPackedUO(0, 0, 0, 0, _getExecuteWithValue(5 ether)), bytes32(0)); + assertEq(plugin.limits(address(acct), 0), 5 ether); + + // uses 5e + 1wei of native tokens + vm.expectRevert( + abi.encodePacked( + UpgradeableModularAccount.PreExecHookReverted.selector, + abi.encode( + address(plugin), + uint8(0), + abi.encodePacked(NativeTokenLimitPlugin.ExceededNativeTokenLimit.selector) + ) + ) + ); + acct.executeUserOp(_getPackedUO(0, 0, 0, 0, _getExecuteWithValue(5 ether + 1)), bytes32(0)); + } + + function test_userOp_executeBatchLimit() public { + Call[] memory calls = new Call[](3); + calls[0] = Call({target: recipient, value: 1, data: ""}); + calls[1] = Call({target: recipient, value: 1 ether, data: ""}); + calls[2] = Call({target: recipient, value: 5 ether + 100000, data: ""}); + + vm.startPrank(address(entryPoint)); + assertEq(plugin.limits(address(acct), 0), 10 ether); + acct.executeUserOp( + _getPackedUO(0, 0, 0, 0, abi.encodeCall(IStandardExecutor.executeBatch, (calls))), bytes32(0) + ); + assertEq(plugin.limits(address(acct), 0), 10 ether - 6 ether - 100001); + assertEq(recipient.balance, 6 ether + 100001); + } + + function test_userOp_combinedExecLimit_success() public { + assertEq(plugin.limits(address(acct), 0), 10 ether); + PackedUserOperation[] memory uos = new PackedUserOperation[](1); + uos[0] = _getPackedUO(100000, 100000, 100000, 1, _getExecuteWithValue(5 ether)); + entryPoint.handleOps(uos, bundler); + + assertEq(plugin.limits(address(acct), 0), 5 ether - 300000); + assertEq(recipient.balance, 5 ether); + } + + function test_userOp_combinedExecBatchLimit_success() public { + Call[] memory calls = new Call[](3); + calls[0] = Call({target: recipient, value: 1, data: ""}); + calls[1] = Call({target: recipient, value: 1 ether, data: ""}); + calls[2] = Call({target: recipient, value: 5 ether + 100000, data: ""}); + + vm.startPrank(address(entryPoint)); + assertEq(plugin.limits(address(acct), 0), 10 ether); + PackedUserOperation[] memory uos = new PackedUserOperation[](1); + uos[0] = _getPackedUO(200000, 200000, 200000, 1, abi.encodeCall(IStandardExecutor.executeBatch, (calls))); + entryPoint.handleOps(uos, bundler); + + assertEq(plugin.limits(address(acct), 0), 10 ether - 6 ether - 700001); + assertEq(recipient.balance, 6 ether + 100001); + } + + function test_userOp_combinedExecLimit_failExec() public { + assertEq(plugin.limits(address(acct), 0), 10 ether); + PackedUserOperation[] memory uos = new PackedUserOperation[](1); + uos[0] = _getPackedUO(100000, 100000, 100000, 1, _getExecuteWithValue(10 ether)); + entryPoint.handleOps(uos, bundler); + + assertEq(plugin.limits(address(acct), 0), 10 ether - 300000); + assertEq(recipient.balance, 0); + } + + function test_runtime_executeLimit() public { + assertEq(plugin.limits(address(acct), 0), 10 ether); + acct.executeWithAuthorization( + _getExecuteWithValue(5 ether), abi.encodePacked(validationFunction, uint8(1)) + ); + assertEq(plugin.limits(address(acct), 0), 5 ether); + } + + function test_runtime_executeBatchLimit() public { + Call[] memory calls = new Call[](3); + calls[0] = Call({target: recipient, value: 1, data: ""}); + calls[1] = Call({target: recipient, value: 1 ether, data: ""}); + calls[2] = Call({target: recipient, value: 5 ether + 100000, data: ""}); + + assertEq(plugin.limits(address(acct), 0), 10 ether); + acct.executeWithAuthorization( + abi.encodeCall(IStandardExecutor.executeBatch, (calls)), abi.encodePacked(validationFunction, uint8(1)) + ); + assertEq(plugin.limits(address(acct), 0), 4 ether - 100001); + } +}