From 90337d64deffab409dd357f348851bd823d2d387 Mon Sep 17 00:00:00 2001 From: adam-alchemy <127769144+adam-alchemy@users.noreply.github.com> Date: Thu, 18 Jan 2024 08:36:03 -0800 Subject: [PATCH] fix: [spearbit-98][quantstamp-8] State is not cached properly before validation/execution steps, Non-Idempotent Pre-Execution Hook (#58) --- src/account/UpgradeableModularAccount.sol | 431 ++--- test/account/AccountExecHooks.t.sol | 4 +- test/account/AccountLoupe.t.sol | 5 + test/account/AccountPermittedCallHooks.t.sol | 4 +- ...gradeableModularAccountPluginManager.t.sol | 2 +- test/account/phases/AccountStatePhases.t.sol | 310 ++++ .../phases/AccountStatePhasesExec.t.sol | 1309 ++++++++++++++++ .../AccountStatePhasesRTValidation.t.sol | 1387 +++++++++++++++++ .../AccountStatePhasesUOValidation.t.sol | 1341 ++++++++++++++++ .../plugins/AccountStateMutatingPlugin.sol | 280 ++++ test/mocks/plugins/ComprehensivePlugin.sol | 3 + test/plugin/TokenReceiverPlugin.t.sol | 6 +- 12 files changed, 4893 insertions(+), 189 deletions(-) create mode 100644 test/account/phases/AccountStatePhases.t.sol create mode 100644 test/account/phases/AccountStatePhasesExec.t.sol create mode 100644 test/account/phases/AccountStatePhasesRTValidation.t.sol create mode 100644 test/account/phases/AccountStatePhasesUOValidation.t.sol create mode 100644 test/mocks/plugins/AccountStateMutatingPlugin.sol diff --git a/src/account/UpgradeableModularAccount.sol b/src/account/UpgradeableModularAccount.sol index 2cc5842da..ec7fc1d4b 100644 --- a/src/account/UpgradeableModularAccount.sol +++ b/src/account/UpgradeableModularAccount.sol @@ -129,28 +129,25 @@ contract UpgradeableModularAccount is /// this function selector, revert. /// @return Data returned from the called execution function. fallback(bytes calldata) external payable returns (bytes memory) { - SelectorData storage selectorData = _getAccountStorage().selectorData[msg.sig]; - - address execPlugin = selectorData.plugin; - if (execPlugin == address(0)) { - revert UnrecognizedFunction(msg.sig); - } - // Either reuse the call buffer from runtime validation, or allocate a new one. It may or may not be used // for pre exec hooks but it will be used for the plugin execution itself. bytes memory callBuffer = (msg.sender != address(_ENTRY_POINT)) ? _doRuntimeValidation() : _allocateRuntimeCallBuffer(msg.data); - bool hasPreExecHooks = selectorData.hasPreExecHooks; - bool hasPostOnlyExecHooks = selectorData.hasPostOnlyExecHooks; - - FunctionReference[] memory postExecHooksToRun; - bytes[] memory postExecHookArgs; - if (hasPreExecHooks) { - // Cache post-exec hooks in memory - (postExecHooksToRun, postExecHookArgs) = _doPreExecHooks(msg.sig, callBuffer); + // To comply with ERC-6900 phase rules, defer the loading of execution phase data until the completion of + // runtime validation. + // Validation may update account state and therefore change execution phase data. These values should also + // be loaded before + // we run the pre exec hooks, because they may modify which plugin is defined. + SelectorData storage selectorData = _getAccountStorage().selectorData[msg.sig]; + address execPlugin = selectorData.plugin; + if (execPlugin == address(0)) { + revert UnrecognizedFunction(msg.sig); } + (FunctionReference[][] memory postHooksToRun, bytes[] memory postHookArgs) = + _doPreExecHooks(selectorData, callBuffer); + // execute the function, bubbling up any reverts bool execSuccess = _executeRaw(execPlugin, _convertRuntimeCallBufferToExecBuffer(callBuffer)); bytes memory execReturnData = _collectReturnData(); @@ -162,14 +159,7 @@ contract UpgradeableModularAccount is } } - _doCachedPostHooks(postExecHooksToRun, postExecHookArgs); - - if (hasPostOnlyExecHooks) { - _doCachedPostHooks( - CastLib.toFunctionReferenceArray(selectorData.executionHooks.postOnlyHooks.getAll()), - new bytes[](0) - ); - } + _doCachedPostHooks(postHooksToRun, postHookArgs); return execReturnData; } @@ -185,13 +175,11 @@ contract UpgradeableModularAccount is revert UserOpNotFromEntryPoint(); } - bool hasPreValidationHooks; - bytes4 selector = _selectorFromCallData(userOp.callData); SelectorData storage selectorData = _getAccountStorage().selectorData[selector]; FunctionReference userOpValidationFunction = selectorData.userOpValidation; - hasPreValidationHooks = selectorData.hasPreUserOpValidationHooks; + bool hasPreValidationHooks = selectorData.hasPreUserOpValidationHooks; validationData = _doUserOpValidation(selector, userOpValidationFunction, userOp, userOpHash, hasPreValidationHooks); @@ -210,14 +198,14 @@ contract UpgradeableModularAccount is override returns (bytes memory result) { - (FunctionReference[] memory postExecHooks, bytes[] memory postExecHookArgs) = _preNativeFunction(); + (FunctionReference[][] memory postExecHooks, bytes[] memory postHookArgs) = _preNativeFunction(); result = _exec(target, value, data); - _postNativeFunction(postExecHooks, postExecHookArgs); + _postNativeFunction(postExecHooks, postHookArgs); } /// @inheritdoc IStandardExecutor function executeBatch(Call[] calldata calls) external payable override returns (bytes[] memory results) { - (FunctionReference[] memory postExecHooks, bytes[] memory postExecHookArgs) = _preNativeFunction(); + (FunctionReference[][] memory postExecHooks, bytes[] memory postHookArgs) = _preNativeFunction(); uint256 callsLength = calls.length; results = new bytes[](callsLength); @@ -230,7 +218,7 @@ contract UpgradeableModularAccount is } } - _postNativeFunction(postExecHooks, postExecHookArgs); + _postNativeFunction(postExecHooks, postHookArgs); } /// @inheritdoc IPluginExecutor @@ -247,28 +235,17 @@ contract UpgradeableModularAccount is bytes memory callBuffer = _allocateRuntimeCallBuffer(data); - FunctionReference[] memory postPermittedCallHooks; - bytes[] memory postPermittedCallHookArgs; - if (permittedCallData.hasPrePermittedCallHooks) { - // Cache post-permitted call hooks in memory - (postPermittedCallHooks, postPermittedCallHookArgs) = - _doPrePermittedCallHooks(permittedCallKey, callBuffer); - } - SelectorData storage selectorData = storage_.selectorData[selector]; + // Load the plugin address from storage prior to running any hooks, to abide by the ERC-6900 phase rules. address execFunctionPlugin = selectorData.plugin; + (FunctionReference[][] memory postHooksToRun, bytes[] memory postHookArgs) = + _doPrePermittedCallHooksAndPreExecHooks(selectorData, permittedCallData, callBuffer); + if (execFunctionPlugin == address(0)) { revert UnrecognizedFunction(selector); } - FunctionReference[] memory postExecHooks; - bytes[] memory postExecHookArgs; - if (selectorData.hasPreExecHooks) { - // Cache post-exec hooks in memory - (postExecHooks, postExecHookArgs) = _doPreExecHooks(selector, callBuffer); - } - bool success = _executeRaw(execFunctionPlugin, _convertRuntimeCallBufferToExecBuffer(callBuffer)); returnData = _collectReturnData(); @@ -278,23 +255,7 @@ contract UpgradeableModularAccount is } } - _doCachedPostHooks(postExecHooks, postExecHookArgs); - - if (selectorData.hasPostOnlyExecHooks) { - _doCachedPostHooks( - CastLib.toFunctionReferenceArray(selectorData.executionHooks.postOnlyHooks.getAll()), - new bytes[](0) - ); - } - - _doCachedPostHooks(postPermittedCallHooks, postPermittedCallHookArgs); - - if (permittedCallData.hasPostOnlyPermittedCallHooks) { - _doCachedPostHooks( - CastLib.toFunctionReferenceArray(permittedCallData.permittedCallHooks.postOnlyHooks.getAll()), - new bytes[](0) - ); - } + _doCachedPostHooks(postHooksToRun, postHookArgs); return returnData; } @@ -347,49 +308,21 @@ contract UpgradeableModularAccount is revert ExecFromPluginExternalNotPermitted(callingPlugin, target, value, data); } - // Run permitted call hooks and execution hooks. `executeFromPluginExternal` doesn't use PermittedCallData - // to check call permissions, nor do they have an address entry in SelectorData, so it doesn't make sense - // to use cached booleans (hasPreExecHooks, hasPostOnlyExecHooks, etc.) to conditionally bypass certain - // steps, as it would just be an added `sload` in the nonzero hooks case. + // Run any pre permitted call hooks specific to this caller and the `executeFromPluginExternal` selector, + // then run any pre-exec hooks associated with the `executeFromPluginExternal` selector. + PermittedCallData storage permittedCallData = storage_.permittedCalls[_getPermittedCallKey( + callingPlugin, IPluginExecutor.executeFromPluginExternal.selector + )]; + SelectorData storage selectorData = + storage_.selectorData[IPluginExecutor.executeFromPluginExternal.selector]; - // Run any pre permitted call hooks specific to this caller and the `executeFromPluginExternal` selector - bytes24 permittedCallKey = - _getPermittedCallKey(callingPlugin, IPluginExecutor.executeFromPluginExternal.selector); - (FunctionReference[] memory postPermittedCallHooks, bytes[] memory postPermittedCallHookArgs) = - _doPrePermittedCallHooks(permittedCallKey, ""); - - // Run any pre exec hooks for the `executeFromPluginExternal` selector - (FunctionReference[] memory postExecHooks, bytes[] memory postExecHookArgs) = - _doPreExecHooks(IPluginExecutor.executeFromPluginExternal.selector, ""); + (FunctionReference[][] memory postHooksToRun, bytes[] memory postHookArgs) = + _doPrePermittedCallHooksAndPreExecHooks(selectorData, permittedCallData, ""); // Perform the external call bytes memory returnData = _exec(target, value, data); - // Run any post exec hooks for the `executeFromPluginExternal` selector - _doCachedPostHooks(postExecHooks, postExecHookArgs); - - // Run any post only exec hooks for the `executeFromPluginExternal` selector - _doCachedPostHooks( - CastLib.toFunctionReferenceArray( - storage_.selectorData[IPluginExecutor.executeFromPluginExternal.selector] - .executionHooks - .postOnlyHooks - .getAll() - ), - new bytes[](0) - ); - - // Run any post permitted call hooks specific to this caller and the `executeFromPluginExternal` selector - _doCachedPostHooks(postPermittedCallHooks, postPermittedCallHookArgs); - - // Run any post only permitted call hooks specific to this caller and the `executeFromPluginExternal` - // selector - _doCachedPostHooks( - CastLib.toFunctionReferenceArray( - storage_.permittedCalls[permittedCallKey].permittedCallHooks.postOnlyHooks.getAll() - ), - new bytes[](0) - ); + _doCachedPostHooks(postHooksToRun, postHookArgs); return returnData; } @@ -402,7 +335,7 @@ contract UpgradeableModularAccount is FunctionReference[] calldata dependencies, InjectedHook[] calldata injectedHooks ) external override { - (FunctionReference[] memory postExecHooks, bytes[] memory postHookArgs) = _preNativeFunction(); + (FunctionReference[][] memory postExecHooks, bytes[] memory postHookArgs) = _preNativeFunction(); _installPlugin(plugin, manifestHash, pluginInitData, dependencies, injectedHooks); _postNativeFunction(postExecHooks, postHookArgs); } @@ -414,7 +347,7 @@ contract UpgradeableModularAccount is bytes calldata pluginUninstallData, bytes[] calldata hookUnapplyData ) external override { - (FunctionReference[] memory postExecHooks, bytes[] memory postHookArgs) = _preNativeFunction(); + (FunctionReference[][] memory postExecHooks, bytes[] memory postHookArgs) = _preNativeFunction(); UninstallPluginArgs memory args; args.plugin = plugin; @@ -455,7 +388,7 @@ contract UpgradeableModularAccount is /// @inheritdoc UUPSUpgradeable function upgradeToAndCall(address newImplementation, bytes calldata data) public payable override onlyProxy { - (FunctionReference[] memory postExecHooks, bytes[] memory postHookArgs) = _preNativeFunction(); + (FunctionReference[][] memory postExecHooks, bytes[] memory postHookArgs) = _preNativeFunction(); UUPSUpgradeable.upgradeToAndCall(newImplementation, data); _postNativeFunction(postExecHooks, postHookArgs); } @@ -476,7 +409,7 @@ contract UpgradeableModularAccount is /// execute, executeBatch, installPlugin, uninstallPlugin. function _preNativeFunction() internal - returns (FunctionReference[] memory postExecHooks, bytes[] memory postExecHookArgs) + returns (FunctionReference[][] memory postExecHooks, bytes[] memory postHookArgs) { bytes memory callBuffer = ""; @@ -484,22 +417,15 @@ contract UpgradeableModularAccount is callBuffer = _doRuntimeValidation(); } - (postExecHooks, postExecHookArgs) = _doPreExecHooks(msg.sig, callBuffer); + (postExecHooks, postHookArgs) = _doPreExecHooks(_getAccountStorage().selectorData[msg.sig], callBuffer); } /// @dev Wraps execution of a native function with runtime validation and hooks. Used for upgradeToAndCall, /// execute, executeBatch, installPlugin, uninstallPlugin. - function _postNativeFunction(FunctionReference[] memory postExecHooks, bytes[] memory postExecHookArgs) + function _postNativeFunction(FunctionReference[][] memory postExecHooks, bytes[] memory postHookArgs) internal { - _doCachedPostHooks(postExecHooks, postExecHookArgs); - - _doCachedPostHooks( - CastLib.toFunctionReferenceArray( - _getAccountStorage().selectorData[msg.sig].executionHooks.postOnlyHooks.getAll() - ), - new bytes[](0) - ); + _doCachedPostHooks(postExecHooks, postHookArgs); } /// @dev To support gas estimation, we don't fail early when the failure is caused by a signature failure. @@ -647,54 +573,162 @@ contract UpgradeableModularAccount is } } - function _doPreExecHooks(bytes4 selector, bytes memory callBuffer) + /// @dev Executes pre-exec hooks and returns the post-exec hooks to run and their associated args. + function _doPreExecHooks(SelectorData storage selectorData, bytes memory callBuffer) internal - returns (FunctionReference[] memory, bytes[] memory) + returns (FunctionReference[][] memory postHooksToRun, bytes[] memory postHookArgs) { - SelectorData storage selectorData = _getAccountStorage().selectorData[selector]; - return _doPreHooks( - selectorData.executionHooks.preHooks, selectorData.executionHooks.associatedPostHooks, callBuffer - ); - } + FunctionReference[] memory preExecHooks; - function _doPrePermittedCallHooks(bytes24 permittedCallKey, bytes memory callBuffer) - internal - returns (FunctionReference[] memory, bytes[] memory) - { - PermittedCallData storage permittedCallData = _getAccountStorage().permittedCalls[permittedCallKey]; - return _doPreHooks( - permittedCallData.permittedCallHooks.preHooks, - permittedCallData.permittedCallHooks.associatedPostHooks, - callBuffer - ); + bool hasPreExecHooks = selectorData.hasPreExecHooks; + bool hasPostOnlyExecHooks = selectorData.hasPostOnlyExecHooks; + + if (hasPreExecHooks) { + preExecHooks = CastLib.toFunctionReferenceArray(selectorData.executionHooks.preHooks.getAll()); + } + + // Allocate memory for the post hooks and post hook args. + // If we have post-only hooks, we allocate an extra FunctionReference[] for them, and one extra element + // in the args for their empty `bytes` argument. + uint256 postHooksToRunLength = preExecHooks.length + (hasPostOnlyExecHooks ? 1 : 0); + postHooksToRun = new FunctionReference[][](postHooksToRunLength); + postHookArgs = new bytes[](postHooksToRunLength); + + uint256 currentIndex = 0; + + if (hasPostOnlyExecHooks) { + // If we have post-only hooks, we allocate an single FunctionReference[] for them, and one element + // in the args for their empty `bytes` argument. We put this into the first element of the post + // hooks in order to have it run last. + postHooksToRun[0] = + CastLib.toFunctionReferenceArray(selectorData.executionHooks.postOnlyHooks.getAll()); + unchecked { + ++currentIndex; + } + } + + // If there are no pre exec hooks, this will short-circuit in the length check on `preExecHooks`. + _cacheAssociatedPostHooks(preExecHooks, selectorData.executionHooks, postHooksToRun, currentIndex); + + // Run all pre-exec hooks and capture their outputs. + _doPreHooks(preExecHooks, callBuffer, postHooksToRun, postHookArgs, currentIndex); } - function _doPreHooks( - LinkedListSet storage preHookSet, - mapping(FunctionReference => LinkedListSet) storage associatedPostHooks, + /// @dev Executes pre-permitted call hooks and pre-exec hooks, and returns the post-exec hooks to run and + /// their associated args. + function _doPrePermittedCallHooksAndPreExecHooks( + SelectorData storage selectorData, + PermittedCallData storage permittedCallData, bytes memory callBuffer - ) internal returns (FunctionReference[] memory postHooks, bytes[] memory postHookArgs) { - FunctionReference[] memory preExecHooks = CastLib.toFunctionReferenceArray(preHookSet.getAll()); + ) internal returns (FunctionReference[][] memory postHooksToRun, bytes[] memory postHookArgs) { + FunctionReference[] memory prePermittedCallHooks; + FunctionReference[] memory preExecHooks; - uint256 preExecHooksLength = preExecHooks.length; - uint256 maxPostHooksToRunLength; + bool hasPrePermittedCallHooks = permittedCallData.hasPrePermittedCallHooks; + bool hasPostOnlyPermittedCallHooks = permittedCallData.hasPostOnlyPermittedCallHooks; - // There can only be as many associated post hooks to run as there are pre hooks. - for (uint256 i = 0; i < preExecHooksLength;) { + bool hasPreExecHooks = selectorData.hasPreExecHooks; + bool hasPostOnlyExecHooks = selectorData.hasPostOnlyExecHooks; + + // If we have any type of pre hooks, we need to allocate memory for them to perform their call. + if (callBuffer.length == 0 && (hasPrePermittedCallHooks || hasPreExecHooks)) { + callBuffer = _allocateRuntimeCallBuffer(msg.data); + } + + if (hasPrePermittedCallHooks) { + prePermittedCallHooks = + CastLib.toFunctionReferenceArray(permittedCallData.permittedCallHooks.preHooks.getAll()); + } + + if (hasPreExecHooks) { + preExecHooks = CastLib.toFunctionReferenceArray(selectorData.executionHooks.preHooks.getAll()); + } + + // Allocate memory for the post hooks and post hook args. + // If we have post-only hooks, we allocate an extra FunctionReference[] for them, and one extra element in + // the args for their empty `bytes` argument. + uint256 postHooksToRunLength = prePermittedCallHooks.length + preExecHooks.length + + (hasPostOnlyPermittedCallHooks ? 1 : 0) + (hasPostOnlyExecHooks ? 1 : 0); + postHooksToRun = new FunctionReference[][](postHooksToRunLength); + postHookArgs = new bytes[](postHooksToRunLength); + + // The order we want to fill the post exec hooks is: + // 1. Post-only permitted call hooks + // 2. Associated post hooks for pre-permitted call hooks + // 3. Post-only exec hooks + // 4. Associated post hooks for pre-exec hooks + + uint256 associatedPermittedCallHooksIndex = 0; + + if (hasPostOnlyPermittedCallHooks) { + // If we have post-only hooks, we allocate an single FunctionReference[] for them, and one element in + // the args for their empty `bytes` argument. We put this into the first element of the post hooks in + // order to have it run last. + postHooksToRun[associatedPermittedCallHooksIndex] = + CastLib.toFunctionReferenceArray(permittedCallData.permittedCallHooks.postOnlyHooks.getAll()); unchecked { - maxPostHooksToRunLength += preHookSet.getCount(CastLib.toSetValue(preExecHooks[i])); - ++i; + ++associatedPermittedCallHooksIndex; + } + } + + // Cache post-permitted-call hooks in memory + _cacheAssociatedPostHooks( + prePermittedCallHooks, + permittedCallData.permittedCallHooks, + postHooksToRun, + associatedPermittedCallHooksIndex + ); + // The exec hooks start after the permitted call hooks + uint256 associatedExecHooksIndex; + unchecked { + associatedExecHooksIndex = associatedPermittedCallHooksIndex + prePermittedCallHooks.length; + } + + if (hasPostOnlyExecHooks) { + // If we have post-only hooks, we allocate an single FunctionReference[] for them, and one element in + // the args for their empty `bytes` argument. We put this into the first element of the post hooks in + // order to have it run last. + postHooksToRun[associatedExecHooksIndex] = + CastLib.toFunctionReferenceArray(selectorData.executionHooks.postOnlyHooks.getAll()); + unchecked { + ++associatedExecHooksIndex; } } - // Overallocate on length, but not all of this may get filled up. - postHooks = new FunctionReference[](maxPostHooksToRunLength); - postHookArgs = new bytes[](maxPostHooksToRunLength); - uint256 actualPostHooksToRunLength; + // Cache post-exec hooks in memory + _cacheAssociatedPostHooks( + preExecHooks, selectorData.executionHooks, postHooksToRun, associatedExecHooksIndex + ); + + // Run the permitted call hooks + _doPreHooks( + prePermittedCallHooks, callBuffer, postHooksToRun, postHookArgs, associatedPermittedCallHooksIndex + ); + + // Run the pre-exec hooks + _doPreHooks(preExecHooks, callBuffer, postHooksToRun, postHookArgs, associatedExecHooksIndex); + } + + /// @dev Execute all pre hooks provided, using the call buffer if provided. + /// Outputs are captured into the `hookReturnData` array, in increasing index starting at `startingIndex`. + /// The `postHooks` array is used to determine whether or not to capture the return data. + /// NOTE: The caller must ensure that: + /// - `postHooks` is allocated, and `startingIndex + preHooks.length` does not exceed the array bounds of + /// `postHooks`. + /// - `hookReturnData` is allocated, and `startingIndex + preHooks.length` does not exceed the array bounds of + /// `hookReturnData`. + function _doPreHooks( + FunctionReference[] memory preHooks, + bytes memory callBuffer, + FunctionReference[][] memory postHooks, // Only used to check if any post hooks exist. + bytes[] memory hookReturnData, + uint256 startingIndex // Where to start writing into hookReturnData + ) internal { + uint256 preExecHooksLength = preHooks.length; // If not running anything, short-circuit before allocating more memory for the call buffers. if (preExecHooksLength == 0) { - return (postHooks, postHookArgs); + return; } if (callBuffer.length == 0) { @@ -706,7 +740,7 @@ contract UpgradeableModularAccount is _updatePluginCallBufferSelector(callBuffer, IPlugin.preExecutionHook.selector); for (uint256 i = 0; i < preExecHooksLength;) { - FunctionReference preExecHook = preExecHooks[i]; + FunctionReference preExecHook = preHooks[i]; if (preExecHook.isEmptyOrMagicValue()) { // The function reference must be the Always Deny magic value in this case, @@ -718,50 +752,54 @@ contract UpgradeableModularAccount is _updatePluginCallBufferFunctionId(callBuffer, functionId); - if (preHookSet.flagsEnabled(CastLib.toSetValue(preExecHook), _PRE_EXEC_HOOK_HAS_POST_FLAG)) { - FunctionReference[] memory associatedPostExecHooks = - CastLib.toFunctionReferenceArray(associatedPostHooks[preExecHook].getAll()); - uint256 associatedPostExecHooksLength = associatedPostExecHooks.length; + _executeRuntimePluginFunction(callBuffer, plugin, PreExecHookReverted.selector); - for (uint256 j = 0; j < associatedPostExecHooksLength;) { - // Execute the pre-hook as many times as there are unique associated post-hooks. - _executeRuntimePluginFunction(callBuffer, plugin, PreExecHookReverted.selector); - - postHooks[actualPostHooksToRunLength] = associatedPostExecHooks[j]; - postHookArgs[actualPostHooksToRunLength] = abi.decode(_collectReturnData(), (bytes)); + uint256 adjustedIndex; + unchecked { + adjustedIndex = startingIndex + i; + } - unchecked { - ++actualPostHooksToRunLength; - ++j; - } - } - } else { - _executeRuntimePluginFunction(callBuffer, plugin, PreExecHookReverted.selector); + // Only collect the return data if there is at least one post-hook to consume it. + if (postHooks[adjustedIndex].length > 0) { + hookReturnData[adjustedIndex] = abi.decode(_collectReturnData(), (bytes)); } unchecked { ++i; } } - - // "Trim" the associated post hook arrays to the actual length, since we may have overallocated. This - // allows for execution of post hooks in reverse order. - assembly ("memory-safe") { - mstore(postHooks, actualPostHooksToRunLength) - mstore(postHookArgs, actualPostHooksToRunLength) - } } - function _doCachedPostHooks(FunctionReference[] memory postHooks, bytes[] memory postHookArgs) internal { - uint256 postHooksToRunLength = postHooks.length; - bool hasPostHookArgs = postHookArgs.length > 0; - for (uint256 i = postHooksToRunLength; i > 0;) { - FunctionReference postExecHook = postHooks[i - 1]; - (address plugin, uint8 functionId) = postExecHook.unpack(); - // solhint-disable-next-line no-empty-blocks - try IPlugin(plugin).postExecutionHook(functionId, hasPostHookArgs ? postHookArgs[i - 1] : bytes("")) {} - catch (bytes memory revertReason) { - revert PostExecHookReverted(plugin, functionId, revertReason); + /// @dev Executes all post hooks in the nested array, using the corresponding args in the nested array. + /// Executes the elements in reverse order, so the caller should ensure the correct ordering before calling. + function _doCachedPostHooks(FunctionReference[][] memory postHooks, bytes[] memory postHookArgs) internal { + // Run post hooks in reverse order of their associated pre hooks. + uint256 postHookArrsLength = postHooks.length; + for (uint256 i = postHookArrsLength; i > 0;) { + uint256 index; + unchecked { + // i starts as the length of the array and goes to 1, not zero, to avoid underflowing. + // To use the index for array access, we need to subtract 1. + index = i - 1; + } + FunctionReference[] memory postHooksToRun = postHooks[index]; + + // We don't need to run each associated post-hook in reverse order, because the associativity we want + // to maintain is reverse order of associated pre-hooks. + uint256 postHooksToRunLength = postHooksToRun.length; + for (uint256 j = 0; j < postHooksToRunLength;) { + (address plugin, uint8 functionId) = postHooksToRun[j].unpack(); + + // Execute the post hook with the current post hook args + // solhint-disable-next-line no-empty-blocks + try IPlugin(plugin).postExecutionHook(functionId, postHookArgs[index]) {} + catch (bytes memory revertReason) { + revert PostExecHookReverted(plugin, functionId, revertReason); + } + + unchecked { + ++j; + } } unchecked { @@ -774,6 +812,35 @@ contract UpgradeableModularAccount is // solhint-disable-next-line no-empty-blocks function _authorizeUpgrade(address newImplementation) internal override {} + /// @dev Loads the associated post hooks for the given pre-exec hooks in the `postHooks` array, starting at + /// `startingIndex`. + /// NOTE: The caller must ensure that `postHooks` is allocated, and `startingIndex + preHooks.length` does not + // exceed the array bounds of `postHooks`. + function _cacheAssociatedPostHooks( + FunctionReference[] memory preExecHooks, + HookGroup storage hookGroup, + FunctionReference[][] memory postHooks, + uint256 startingIndex + ) internal view { + uint256 preExecHooksLength = preExecHooks.length; + for (uint256 i = 0; i < preExecHooksLength;) { + FunctionReference preExecHook = preExecHooks[i]; + + // If the pre-exec hook has associated post hooks, cache them in the postHooks array. + if (hookGroup.preHooks.flagsEnabled(CastLib.toSetValue(preExecHook), _PRE_EXEC_HOOK_HAS_POST_FLAG)) { + // We start writing into the postHooks array starting at the `startingIndex` and counting up. + postHooks[startingIndex + i] = + CastLib.toFunctionReferenceArray(hookGroup.associatedPostHooks[preExecHook].getAll()); + } + // In no-associated-post-hooks case, we're OK returning the default value, which is an array of length + // 0. + + unchecked { + ++i; + } + } + } + /// @dev Revert with an appropriate error if the calldata does not include a function selector. function _selectorFromCallData(bytes calldata data) internal pure returns (bytes4) { if (data.length < 4) { diff --git a/test/account/AccountExecHooks.t.sol b/test/account/AccountExecHooks.t.sol index 1130fc265..0df93c749 100644 --- a/test/account/AccountExecHooks.t.sol +++ b/test/account/AccountExecHooks.t.sol @@ -728,7 +728,7 @@ contract UpgradeableModularAccountExecHooksTest is Test { /// @dev Plugin 1 hook pair: [1, 2] /// Plugin 2 hook pair: [1, 4] - /// Expected execution: [1, 2], [1, 4] + /// Expected execution: [1, 2], [null, 4] function test_overlappingExecHookPairsOnPre_run() public { test_overlappingExecHookPairsOnPre_install(); @@ -744,7 +744,7 @@ contract UpgradeableModularAccountExecHooksTest is Test { 0, // msg.value in call to account abi.encodeWithSelector(_EXEC_SELECTOR) ), - 2 + 1 ); // Expect each post hook to be called once, with the expected data. diff --git a/test/account/AccountLoupe.t.sol b/test/account/AccountLoupe.t.sol index efbf8702c..0127034b9 100644 --- a/test/account/AccountLoupe.t.sol +++ b/test/account/AccountLoupe.t.sol @@ -567,4 +567,9 @@ contract AccountLoupeTest is Test { assertEq(FunctionReference.unwrap(hook.preExecHook), FunctionReference.unwrap(preHook)); assertEq(FunctionReference.unwrap(hook.postExecHook), FunctionReference.unwrap(postHook)); } + + function test_trace_comprehensivePlugin() public { + vm.prank(address(comprehensivePlugin)); + account1.executeFromPlugin(abi.encodeCall(comprehensivePlugin.foo, ())); + } } diff --git a/test/account/AccountPermittedCallHooks.t.sol b/test/account/AccountPermittedCallHooks.t.sol index 11fc86bf4..7bef307ee 100644 --- a/test/account/AccountPermittedCallHooks.t.sol +++ b/test/account/AccountPermittedCallHooks.t.sol @@ -537,7 +537,7 @@ contract UpgradeableModularAccountPermittedCallHooksTest is Test { } /// @dev Plugin hook pair(s): [1, 2], [1, 4] - /// Expected execution: [1, 2], [1, 4] + /// Expected execution: [1, 2], [null, 4] function test_overlappingPermittedCallHookPairsOnPre_run() public { test_overlappingPermittedCallHookPairsOnPre_install(); @@ -553,7 +553,7 @@ contract UpgradeableModularAccountPermittedCallHooksTest is Test { 0, // msg.value in call to account abi.encodePacked(_EXEC_SELECTOR) ), - 2 + 1 ); // Expect each post hook to be called once, with the expected data. diff --git a/test/account/UpgradeableModularAccountPluginManager.t.sol b/test/account/UpgradeableModularAccountPluginManager.t.sol index ac1ce1b78..c70c17123 100644 --- a/test/account/UpgradeableModularAccountPluginManager.t.sol +++ b/test/account/UpgradeableModularAccountPluginManager.t.sol @@ -537,7 +537,7 @@ contract UpgradeableModularAccountPluginManagerTest is Test { // manifest for uninstallation despite being given the old one). vm.expectRevert( abi.encodeWithSelector( - UpgradeableModularAccount.UnrecognizedFunction.selector, + UpgradeableModularAccount.RuntimeValidationFunctionMissing.selector, CanChangeManifestPlugin.someExecutionFunction.selector ) ); diff --git a/test/account/phases/AccountStatePhases.t.sol b/test/account/phases/AccountStatePhases.t.sol new file mode 100644 index 000000000..5b0a1880c --- /dev/null +++ b/test/account/phases/AccountStatePhases.t.sol @@ -0,0 +1,310 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +import {Test} from "forge-std/Test.sol"; + +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {EntryPoint} from "@eth-infinitism/account-abstraction/core/EntryPoint.sol"; + +import {UpgradeableModularAccount} from "../../../src/account/UpgradeableModularAccount.sol"; +import {MultiOwnerPlugin} from "../../../src/plugins/owner/MultiOwnerPlugin.sol"; +import {IEntryPoint} from "../../../src/interfaces/erc4337/IEntryPoint.sol"; +import {UserOperation} from "../../../src/interfaces/erc4337/UserOperation.sol"; +import {IPluginManager} from "../../../src/interfaces/IPluginManager.sol"; +import {IStandardExecutor, Call} from "../../../src/interfaces/IStandardExecutor.sol"; +import { + IPlugin, + ManifestExecutionHook, + PluginManifest, + ManifestFunction, + ManifestAssociatedFunctionType, + ManifestAssociatedFunction +} from "../../../src/interfaces/IPlugin.sol"; +import {FunctionReference, FunctionReferenceLib} from "../../../src/libraries/FunctionReferenceLib.sol"; +import {MultiOwnerMSCAFactory} from "../../../src/factory/MultiOwnerMSCAFactory.sol"; + +import {AccountStateMutatingPlugin} from "../../mocks/plugins/AccountStateMutatingPlugin.sol"; +import {MockPlugin} from "../../mocks/MockPlugin.sol"; + +// A test suite that verifies how the account caches the state of plugins. This is intended to ensure consistency +// of execution flow when either hooks or plugins change installation state within a single call to the account. +// The following tests inherit from this test base: +// - AccountStatePhasesUOValidationTest +// - AccountStatePhasesRTValidationTest +// - AccountStatePhasesExecTest +// NOTE: This test implicitly depends on hooks execution order being latest-to-oldest. This is not guaranteed by +// the spec, but is currently the case. If that changes, this test will need to be updated. +// How these tests will work +// - Create a custom plugin "AccountStateMutatingPlugin" that can perform install / uninstall during hooks, +// validation, or execution. +// - This is done by pushing the call encoding responsibilitiy to this test, and just exposing a "side" +// method that specifies the callback it should do in a given phase back toward the calling account. +// - Authorization for install/uninstall can be granted by making the plugin itself an owner in multi-owner +// plugin, which will authorize runtime calls. +// - The contents of what is called are defined in a mock plugin like the exec hooks test. +contract AccountStatePhasesTest is Test { + using ECDSA for bytes32; + + IEntryPoint public entryPoint; + MultiOwnerPlugin public multiOwnerPlugin; + MultiOwnerMSCAFactory public factory; + address payable beneficiary; + + address public owner1; + uint256 public owner1Key; + UpgradeableModularAccount public account1; + + AccountStateMutatingPlugin public asmPlugin; + + MockPlugin public mockPlugin1; + bytes32 public manifestHash1; + PluginManifest public m1; + + // Function ID constants to use with the mock plugin. + uint8 internal constant _PRE_HOOK_FUNCTION_ID_1 = 1; + uint8 internal constant _POST_HOOK_FUNCTION_ID_2 = 2; + uint8 internal constant _PRE_UO_VALIDATION_HOOK_FUNCTION_ID_3 = 3; + uint8 internal constant _PRE_RT_VALIDATION_HOOK_FUNCTION_ID_4 = 4; + uint8 internal constant _UO_VALIDATION_FUNCTION_ID_5 = 5; + uint8 internal constant _RT_VALIDATION_FUNCTION_ID_6 = 6; + + // Event re-declarations for vm.expectEmit + event PluginInstalled( + address indexed plugin, + bytes32 manifestHash, + FunctionReference[] dependencies, + IPluginManager.InjectedHook[] injectedHooks + ); + event PluginUninstalled(address indexed plugin, bool indexed callbacksSucceeded); + event ReceivedCall(bytes msgData, uint256 msgValue); + + // Empty arrays for convenience + FunctionReference[] internal _EMPTY_DEPENDENCIES; + IPluginManager.InjectedHook[] internal _EMPTY_INJECTED_HOOKS; + bytes[] internal _EMPTY_HOOK_APPLY_DATA; + + // Constants for running user ops + uint256 constant CALL_GAS_LIMIT = 300000; + uint256 constant VERIFICATION_GAS_LIMIT = 1000000; + + function setUp() public { + entryPoint = IEntryPoint(address(new EntryPoint())); + multiOwnerPlugin = new MultiOwnerPlugin(); + asmPlugin = new AccountStateMutatingPlugin(); + + (owner1, owner1Key) = makeAddrAndKey("owner1"); + beneficiary = payable(makeAddr("beneficiary")); + address accountImpl = address(new UpgradeableModularAccount(IEntryPoint(address(entryPoint)))); + + factory = new MultiOwnerMSCAFactory( + address(this), + address(multiOwnerPlugin), + accountImpl, + keccak256(abi.encode(multiOwnerPlugin.pluginManifest())), + entryPoint + ); + + // Add 2 owners to the account: + // - The owner1 EOA, for signing user operations + // - The AccountStateMutatingPlugin, for authorizing runtime calls to installPlugin/uninstallPlugin + address[] memory owners = new address[](2); + owners[0] = owner1; + owners[1] = address(asmPlugin); + account1 = UpgradeableModularAccount(payable(factory.createAccount(0, owners))); + vm.deal(address(account1), 100 ether); + } + + // HELPER FUNCTIONS + + // Mock plugin config helpers - shortcuts to configure with 1 plugin function. + // Does not install the mock plugin. + + function _initMockPluginPreUserOpValidationHook() internal { + m1.preUserOpValidationHooks.push( + ManifestAssociatedFunction({ + executionSelector: AccountStateMutatingPlugin.executionFunction.selector, + associatedFunction: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: _PRE_UO_VALIDATION_HOOK_FUNCTION_ID_3, + dependencyIndex: 0 // unused + }) + }) + ); + _initMockPlugin(); + } + + function _initMockPluginUserOpValidationFunction() internal { + m1.userOpValidationFunctions.push( + ManifestAssociatedFunction({ + executionSelector: AccountStateMutatingPlugin.executionFunction.selector, + associatedFunction: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: _UO_VALIDATION_FUNCTION_ID_5, + dependencyIndex: 0 // unused + }) + }) + ); + _initMockPlugin(); + } + + function _initMockPluginPreRuntimeValidationHook() internal { + m1.preRuntimeValidationHooks.push( + ManifestAssociatedFunction({ + executionSelector: AccountStateMutatingPlugin.executionFunction.selector, + associatedFunction: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: _PRE_RT_VALIDATION_HOOK_FUNCTION_ID_4, + dependencyIndex: 0 // unused + }) + }) + ); + _initMockPlugin(); + } + + function _initMockPluginRuntimeValidationFunction() internal { + m1.runtimeValidationFunctions.push( + ManifestAssociatedFunction({ + executionSelector: AccountStateMutatingPlugin.executionFunction.selector, + associatedFunction: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: _RT_VALIDATION_FUNCTION_ID_6, + dependencyIndex: 0 // unused + }) + }) + ); + _initMockPlugin(); + } + + function _initMockPluginPreExecutionHook() internal { + m1.executionHooks.push( + ManifestExecutionHook({ + executionSelector: AccountStateMutatingPlugin.executionFunction.selector, + preExecHook: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: _PRE_HOOK_FUNCTION_ID_1, + dependencyIndex: 0 // Unused + }), + postExecHook: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.NONE, + functionId: 0, // Unused + dependencyIndex: 0 // Unused + }) + }) + ); + _initMockPlugin(); + } + + function _initMockPluginExecFunction() internal { + m1.executionFunctions.push(AccountStateMutatingPlugin.executionFunction.selector); + _initMockPlugin(); + } + + function _initMockPluginPreAndPostExecutionHook() internal { + m1.executionHooks.push( + ManifestExecutionHook({ + executionSelector: AccountStateMutatingPlugin.executionFunction.selector, + preExecHook: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: _PRE_HOOK_FUNCTION_ID_1, + dependencyIndex: 0 // Unused + }), + postExecHook: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: _POST_HOOK_FUNCTION_ID_2, + dependencyIndex: 0 // Unused + }) + }) + ); + _initMockPlugin(); + } + + function _initMockPluginPostOnlyExecutionHook() internal { + m1.executionHooks.push( + ManifestExecutionHook({ + executionSelector: AccountStateMutatingPlugin.executionFunction.selector, + preExecHook: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.NONE, + functionId: 0, // Unused + dependencyIndex: 0 // Unused + }), + postExecHook: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: _POST_HOOK_FUNCTION_ID_2, + dependencyIndex: 0 // Unused + }) + }) + ); + _initMockPlugin(); + } + + // Installs the account state mutating plugin. Prior to calling this, the test should configure the desired + // plugin functions and callbacks, since the manifest will change based on that configuration. + function _installASMPlugin() internal { + bytes32 manifestHash = _manifestHashOf(asmPlugin.pluginManifest()); + vm.expectEmit(true, true, true, true); + emit PluginInstalled(address(asmPlugin), manifestHash, _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS); + vm.prank(owner1); + account1.installPlugin(address(asmPlugin), manifestHash, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS); + } + + // Sets up the manifest hash variable and deploys the mock plugin. + function _initMockPlugin() internal { + manifestHash1 = _manifestHashOf(m1); + mockPlugin1 = new MockPlugin(m1); + } + + // Installs the mock plugin onto the account. Prior to calling this, the test should configure the desired + // plugin functions, and call _initMockPlugin() to set up the mock plugin. + function _installMockPlugin() internal { + vm.expectEmit(true, true, true, true); + emit ReceivedCall(abi.encodeCall(IPlugin.onInstall, (bytes(""))), 0); + vm.expectEmit(true, true, true, true); + emit PluginInstalled(address(mockPlugin1), manifestHash1, _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS); + vm.prank(owner1); + account1.installPlugin(address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS); + } + + function _manifestHashOf(PluginManifest memory manifest) internal pure returns (bytes32) { + return keccak256(abi.encode(manifest)); + } + + function _generateAndSignUserOp() internal view returns (UserOperation[] memory ops) { + ops = new UserOperation[](1); + ops[0] = UserOperation({ + sender: address(account1), + nonce: entryPoint.getNonce(address(account1), 0), + initCode: "", + callData: abi.encodeCall(AccountStateMutatingPlugin.executionFunction, ()), + callGasLimit: CALL_GAS_LIMIT, + verificationGasLimit: VERIFICATION_GAS_LIMIT, + preVerificationGas: 0, + maxFeePerGas: 2, + maxPriorityFeePerGas: 1, + paymasterAndData: "", + signature: "" + }); + + bytes32 userOpHash = entryPoint.getUserOpHash(ops[0]); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); + ops[0].signature = abi.encodePacked(r, s, v); + } + + function _generateCallsUninstallASMInstallMock() internal view returns (Call[] memory) { + // Encode two self-calls: one to uninstall ASM plugin, one to install the mock plugin. + Call[] memory calls = new Call[](2); + calls[0] = Call({ + target: address(account1), + value: 0 ether, + data: abi.encodeCall(IPluginManager.uninstallPlugin, (address(asmPlugin), "", "", _EMPTY_HOOK_APPLY_DATA)) + }); + calls[1] = Call({ + target: address(account1), + value: 0 ether, + data: abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ) + }); + return calls; + } +} diff --git a/test/account/phases/AccountStatePhasesExec.t.sol b/test/account/phases/AccountStatePhasesExec.t.sol new file mode 100644 index 000000000..8d781f891 --- /dev/null +++ b/test/account/phases/AccountStatePhasesExec.t.sol @@ -0,0 +1,1309 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +import {AccountStatePhasesTest} from "./AccountStatePhases.t.sol"; + +import {IPluginManager} from "../../../src/interfaces/IPluginManager.sol"; +import { + IPlugin, + PluginManifest, + ManifestExecutionHook, + ManifestAssociatedFunction, + ManifestAssociatedFunctionType, + ManifestFunction +} from "../../../src/interfaces/IPlugin.sol"; +import {IStandardExecutor, Call} from "../../../src/interfaces/IStandardExecutor.sol"; + +import {AccountStateMutatingPlugin} from "../../mocks/plugins/AccountStateMutatingPlugin.sol"; +import {MockPlugin} from "../../mocks/MockPlugin.sol"; + +// Tests the account state phase behavior when the source of the state modification happens during execution. +contract AccountStatePhasesUOValidationTest is AccountStatePhasesTest { + // Test cases covered here + // These are listed in the order they are run in the test suite. + // The "source" indicates which in which phase the plugin will perform a modification, and the "target" + // indicates which phase will change as a result of the modification. + // + // - Source: pre-Exec + // - Target: pre-UserOp-Validation + // - n/a - runs before + // - Target: UserOp-Validation + // - n/a - runs before + // - Target: pre-Runtime-Validation + // - n/a - runs before + // - Target: Runtime-Validation + // - n/a - runs before + // - Target: pre-Exec (same phase) + // - Addition (first element): *impossible* + // - Addition (not first): should *not* run + // - Removal: should still run + // - Target: Exec (same phase) + // - Replace: original should run + // - Removal: original should run + // - Target: post-Exec (same phase) + // - Addition (associated, first pre-exec): *impossible* + // - Addition (associated, non-first pre-exec): should *not* run + // - Removal (associated, first pre-exec): *impossible* + // - Removal (associated, non-first pre-exec): should still run + // - Addition (first post-only): should *not* run + // - Addition (non-first post-only): should *not* run + // - Removal (first post-only): should still run + // - Removal (non-first post-only): should still run + // - Source: Exec + // - Target: pre-UserOp-Validation + // - n/a - runs before + // - Target: UserOp-Validation + // - n/a - runs before + // - Target: pre-Runtime-Validation + // - n/a - runs before + // - Target: Runtime-Validation + // - n/a - runs before + // - Target: pre-Exec (same phase) + // - n/a - runs before + // - Target: Exec (same phase) + // - Won’t test, since it’s the same single-element field. + // - Target: post-Exec (same phase) + // - Addition (associated, first pre-exec): should *not* run + // - Addition (associated, non-first pre-exec): should *not* run + // - Removal (associated, first pre-exec): should still run + // - Removal (associated, non-first pre-exec): should still run + // - Addition (first post-only): should *not* run + // - Addition (non-first post-only): should *not* run + // - Removal (first post-only): should still run + // - Removal (non-first post-only): should still run + // - Source: post-Exec + // - Target: pre-UserOp-Validation + // - n/a - runs before + // - Target: UserOp-Validation + // - n/a - runs before + // - Target: pre-Runtime-Validation + // - n/a - runs before + // - Target: Runtime-Validation + // - n/a - runs before + // - Target: pre-Exec (same phase) + // - n/a - runs before + // - Target: Exec (same phase) + // - n/a - runs before + // - Target: post-Exec (same phase) + // - Addition (associated, first pre-exec): should *not* run + // - Addition (associated, non-first pre-exec): should *not* run + // - Removal (associated, first pre-exec): should still run + // - Removal (associated, non-first pre-exec): should still run + // - Addition (first post-only): should *not* run + // - Addition (non-first post-only): should *not* run + // - Removal (first post-only): should still run + // - Removal (non-first post-only): should still run + + // Source: pre-Exec + // Target: pre-UserOp-Validation + // n/a - runs before + + // Source: pre-Exec + // Target: UserOp-Validation + // n/a - runs before + + // Source: pre-Exec + // Target: pre-Runtime-Validation + // n/a - runs before + + // Source: pre-Exec + // Target: Runtime-Validation + // n/a - runs before + + // Source: pre-Exec + // Target: pre-Exec (same phase) + // Addition (first element): *impossible* + + function test_ASP_preExec_add_preExec_notFirstElement() public { + // Source: pre-Exec + // Target: pre-Exec (same phase) + // Addition (not first): should *not* run + + // Set up the mock plugin with a pre-Exec hook, which will be added and should not run. + _initMockPluginPreExecutionHook(); + + // Install the ASM plugin with a pre exec hook that will add a pre exec hook. + // It also needs a pre exec hook to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: true, + setPostExec: false, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.PRE_EXECUTION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // both user op and runtime validation. This will trigger the ASM plugin's pre exec hook to install the + // mock plugin's pre exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preExec_remove_preExec() public { + // Source: pre-Exec + // Target: pre-Exec (same phase) + // Removal: should still run + + // Set up the mock plugin with a pre-Exec hook, which will be removed and should still run. + _initMockPluginPreExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a pre exec hook that will remove the mock plugin's pre exec hook. + // It also needs a pre exec hook to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: true, + setPostExec: false, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_EXECUTION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // both user op and runtime validation. This will trigger the ASM plugin's pre exec hook to remove the + // mock plugin's pre exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should still run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preExec_replace_exec() public { + // Source: pre-Exec + // Target: Exec (same phase) + // Replace: original should run + + // Set up the mock plugin with an Exec function, which will replace the one defined by the ASM plugin + // and should not be run. + _initMockPluginExecFunction(); + + // Install the ASM plugin with a pre exec hook that will replace the exec function. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: true, + setPostExec: false, + setRTValidation: false, + setPreRTValidation: false + }); + // Encode two self-calls: one to uninstall ASM plugin, one to install the mock plugin. + Call[] memory calls = _generateCallsUninstallASMInstallMock(); + asmPlugin.setCallback( + abi.encodeCall(IStandardExecutor.executeBatch, (calls)), + AccountStateMutatingPlugin.FunctionId.PRE_EXECUTION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // both user op and runtime validation. This will trigger the ASM plugin's pre exec hook to replace the + // exec function with the mock plugin's exec function. The original should run, not the replacement. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(AccountStateMutatingPlugin.executionFunction.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(AccountStateMutatingPlugin.executionFunction.selector), + 0 // Should be called 0 times + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preExec_remove_exec() public { + // Source: pre-Exec + // Target: Exec (same phase) + // Removal: original should run + + // Install the ASM plugin with a pre exec hook that will remove the exec function. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: true, + setPostExec: false, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(asmPlugin), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_EXECUTION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // both user op and runtime validation. This will trigger the ASM plugin's pre exec hook to remove the + // exec function. This NOT cause the call to revert, due to being in the same phase. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(AccountStateMutatingPlugin.executionFunction.selector), + 1 // Should be called 1 time + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + // Source: pre-Exec + // Target: post-Exec (same phase) + // Addition (associated, first pre-exec): *impossible* + + function test_ASP_preExec_add_postExec_associated_notFirstElement() public { + // Source: pre-Exec + // Target: post-Exec (same phase) + // Addition (associated, non-first pre-exec): should *not* run + + // Set up the mock plugin with an associated post-Exec hook, which will be added and should not run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the ASM plugin with a pre exec hook that will add a post exec hook. + // It also needs a pre exec hook to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: true, + setPostExec: false, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.PRE_EXECUTION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // both user op and runtime validation. This will trigger the ASM plugin's pre exec hook to install the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + // Source: pre-Exec + // Target: post-Exec (same phase) + // Removal (associated, first pre-exec): *impossible* + + function test_ASP_preExec_remove_postExec_associated_notFirstElement() public { + // Source: pre-Exec + // Target: post-Exec (same phase) + // Removal (associated, non-first pre-exec): should still run + + // Set up the mock plugin with an associated post-Exec hook, which will be removed and should still run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a pre exec hook that will remove the mock plugin's post exec hook. + // It also needs a pre exec hook to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: true, + setPostExec: false, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_EXECUTION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // both user op and runtime validation. This will trigger the ASM plugin's pre exec hook to remove the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should still run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preExec_add_postExec_firstElement() public { + // Source: pre-Exec + // Target: post-Exec (same phase) + // Addition (first post-only): should *not* run + + // Set up the mock plugin with a post-Exec hook, which will be added and should not run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the ASM plugin with a pre exec hook that will add a post exec hook. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: true, + setPostExec: false, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.PRE_EXECUTION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // both user op and runtime validation. This will trigger the ASM plugin's pre exec hook to install the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preExec_add_postExec_notFirstElement() public { + // Source: pre-Exec + // Target: post-Exec (same phase) + // Addition (non-first post-only): should *not* run + + // Set up the mock plugin with a post-Exec hook, which will be added and should not run. + _initMockPluginPostOnlyExecutionHook(); + + // Since the ASM plugin can't define a post-only hook due to using a pre exec hook for its action, we + // need to add another mock plugin to add the first post-only exec hook, in order to test this case. + + PluginManifest memory m2; + m2.executionHooks = new ManifestExecutionHook[](1); + m2.executionHooks[0] = ManifestExecutionHook({ + executionSelector: AccountStateMutatingPlugin.executionFunction.selector, + preExecHook: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.NONE, + functionId: 0, // Unused + dependencyIndex: 0 // Unused + }), + postExecHook: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: _POST_HOOK_FUNCTION_ID_2, + dependencyIndex: 0 // Unused + }) + }); + bytes32 manifestHash2 = _manifestHashOf(m2); + MockPlugin mockPlugin2 = new MockPlugin(m2); + + vm.expectEmit(true, true, true, true); + emit ReceivedCall(abi.encodeCall(IPlugin.onInstall, (bytes(""))), 0); + vm.expectEmit(true, true, true, true); + emit PluginInstalled(address(mockPlugin2), manifestHash2, _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS); + vm.prank(owner1); + account1.installPlugin(address(mockPlugin2), manifestHash2, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS); + + // Install the ASM plugin with a pre exec hook that will add a post exec hook. + // It also needs a post-only exec hook to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: true, + setPostExec: true, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.PRE_EXECUTION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // both user op and runtime validation. This will trigger the ASM plugin's pre exec hook to install the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.expectCall( + address(mockPlugin2), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preExec_remove_postExec_firstElement() public { + // Source: pre-Exec + // Target: post-Exec (same phase) + // Removal (first post-only): should still run + + // Set up the mock plugin with a post-Exec hook, which will be removed and should still run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a pre exec hook that will remove the mock plugin's post exec hook. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: true, + setPostExec: false, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_EXECUTION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // both user op and runtime validation. This will trigger the ASM plugin's pre exec hook to remove the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should still run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preExec_remove_postExec_notFirstElement() public { + // Source: pre-Exec + // Target: post-Exec (same phase) + // Removal (non-first post-only): should still run + + // Since the ASM plugin can't define a post-only hook due to using a pre exec hook for its action, we + // need to add another mock plugin to add the first post-only exec hook, in order to test this case. + + PluginManifest memory m2; + m2.executionHooks = new ManifestExecutionHook[](1); + m2.executionHooks[0] = ManifestExecutionHook({ + executionSelector: AccountStateMutatingPlugin.executionFunction.selector, + preExecHook: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.NONE, + functionId: 0, // Unused + dependencyIndex: 0 // Unused + }), + postExecHook: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: _POST_HOOK_FUNCTION_ID_2, + dependencyIndex: 0 // Unused + }) + }); + bytes32 manifestHash2 = _manifestHashOf(m2); + MockPlugin mockPlugin2 = new MockPlugin(m2); + + vm.expectEmit(true, true, true, true); + emit ReceivedCall(abi.encodeCall(IPlugin.onInstall, (bytes(""))), 0); + vm.expectEmit(true, true, true, true); + emit PluginInstalled(address(mockPlugin2), manifestHash2, _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS); + vm.prank(owner1); + account1.installPlugin(address(mockPlugin2), manifestHash2, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS); + + // Set up the mock plugin with a post-Exec hook, which will be removed and should still run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a pre exec hook that will remove the mock plugin's post exec hook. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: true, + setPostExec: false, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_EXECUTION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // both user op and runtime validation. This will trigger the ASM plugin's pre exec hook to remove the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should still run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(mockPlugin2), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + // Source: Exec + // Target: pre-UserOp-Validation + // n/a - runs before + + // Source: Exec + // Target: UserOp-Validation + // n/a - runs before + + // Source: Exec + // Target: pre-Runtime-Validation + // n/a - runs before + + // Source: Exec + // Target: Runtime-Validation + // n/a - runs before + + // Source: Exec + // Target: pre-Exec (same phase) + // n/a - runs before + + // Source: Exec + // Target: Exec (same phase) + // Won’t test, since it’s the same single-element field. + + function test_ASP_exec_add_postExec_associated_firstElement() public { + // Source: Exec + // Target: post-Exec (same phase) + // Addition (associated, first pre-exec): should *not* run + + // Set up the mock plugin with an associated post-Exec hook, which will be added and should not run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the ASM plugin with an exec function that will add a post exec hook. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: false, + setPostExec: false, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.EXECUTION_FUNCTION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // user op and runtime validation. This will trigger the ASM plugin's exec function to install the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_exec_add_postExec_associated_notFirstElement() public { + // Source: Exec + // Target: post-Exec (same phase) + // Addition (associated, non-first pre-exec): should *not* run + + // Set up the mock plugin with an associated post-Exec hook, which will be added and should not run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the ASM plugin with an exec function that will add a post exec hook. + // It also needs a post exec hook to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: true, + setPostExec: true, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.EXECUTION_FUNCTION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // user op and runtime validation. This will trigger the ASM plugin's exec function to install the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_exec_remove_postExec_associated_firstElement() public { + // Source: Exec + // Target: post-Exec (same phase) + // Removal (associated, first pre-exec): should still run + + // Set up the mock plugin with an associated post-Exec hook, which will be removed and should still run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with an exec function that will remove the mock plugin's post exec hook. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: false, + setPostExec: false, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.EXECUTION_FUNCTION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // user op and runtime validation. This will trigger the ASM plugin's exec function to remove the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should still run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_exec_remove_postExec_associated_notFirstElement() public { + // Source: Exec + // Target: post-Exec (same phase) + // Removal (associated, non-first pre-exec): should still run + + // Set up the mock plugin with an associated post-Exec hook, which will be removed and should still run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with an exec function that will remove the mock plugin's post exec hook. + // It also needs an associated post exec hook to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: true, + setPostExec: true, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.EXECUTION_FUNCTION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // user op and runtime validation. This will trigger the ASM plugin's exec function to remove the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should still run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_exec_add_postExec_firstElement() public { + // Source: Exec + // Target: post-Exec (same phase) + // Addition (first post-only): should *not* run + + // Set up the mock plugin with a post-Exec hook, which will be added and should not run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the ASM plugin with an exec function that will add a post exec hook. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: false, + setPostExec: false, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.EXECUTION_FUNCTION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // user op and runtime validation. This will trigger the ASM plugin's exec function to install the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_exec_add_postExec_notFirstElement() public { + // Source: Exec + // Target: post-Exec (same phase) + // Addition (non-first post-only): should *not* run + + // Set up the mock plugin with a post-Exec hook, which will be added and should not run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the ASM plugin with an exec function that will add a post exec hook. + // It also needs a post exec hook to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: false, + setPostExec: true, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.EXECUTION_FUNCTION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // user op and runtime validation. This will trigger the ASM plugin's exec function to install the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_exec_remove_postExec_firstElement() public { + // Source: Exec + // Target: post-Exec (same phase) + // Removal (first post-only): should still run + + // Set up the mock plugin with a post-Exec hook, which will be removed and should still run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with an exec function that will remove the mock plugin's post exec hook. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: false, + setPostExec: false, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.EXECUTION_FUNCTION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // user op and runtime validation. This will trigger the ASM plugin's exec function to remove the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should still run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_exec_remove_postExec_notFirstElement() public { + // Source: Exec + // Target: post-Exec (same phase) + // Removal (non-first post-only): should still run + + // Set up the mock plugin with a post-Exec hook, which will be removed and should still run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with an exec function that will remove the mock plugin's post exec hook. + // It also needs a post exec hook to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: false, + setPostExec: true, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.EXECUTION_FUNCTION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // user op and runtime validation. This will trigger the ASM plugin's exec function to remove the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should still run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + // Source: post-Exec + // Target: pre-UserOp-Validation + // n/a - runs before + + // Source: post-Exec + // Target: UserOp-Validation + // n/a - runs before + + // Source: post-Exec + // Target: pre-Runtime-Validation + // n/a - runs before + + // Source: post-Exec + // Target: Runtime-Validation + // n/a - runs before + + // Source: post-Exec + // Target: pre-Exec + // n/a - runs before + + // Source: post-Exec + // Target: Exec + // n/a - runs before + + // Source: post-Exec + // Target: post-Exec (same phase) + // Addition (associated, first pre-exec): impossible with the current order of running post-only exec hooks + // after associated post hooks. + + function test_ASP_postExec_add_postExec_associated_notFirstElement() public { + // Source: post-Exec + // Target: post-Exec (same phase) + // Addition (associated, non-first pre-exec): should *not* run + + // Set up the mock plugin with an associated post-Exec hook, which will be added and should not run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the ASM plugin with a post exec hook that will add a post exec hook. + // To ensure the ASM plugin's post exec hook runs first, it needs to be associated, so we also define an + // empty pre-exec hook. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: true, + setPostExec: true, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.POST_EXECUTION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // user op and runtime validation. This will trigger the ASM plugin's post exec hook to install the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + // Source: post-Exec + // Target: post-Exec (same phase) + // Removal (associated, first pre-exec): impossible with the current order of running post-only exec hooks + // after associated post hooks. + + function test_ASP_postExec_remove_postExec_associated_notFirstElement() public { + // Source: post-Exec + // Target: post-Exec (same phase) + // Removal (associated, non-first pre-exec): should still run + + // Set up the mock plugin with an associated post-Exec hook, which will be removed and should still run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a post exec hook that will remove the mock plugin's post exec hook. + // To ensure the ASM plugin's post exec hook runs first, it needs to be associated, so we also define an + // empty pre-exec hook. This also ensures it is not the first post exec hook. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: true, + setPostExec: true, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.POST_EXECUTION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // user op and runtime validation. This will trigger the ASM plugin's post exec hook to remove the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // ASM plugin's hook should still run. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_postExec_add_postExec_firstElement() public { + // Source: post-Exec + // Target: post-Exec (same phase) + // Addition (first post-only): should *not* run + + // Set up the mock plugin with a post-Exec hook, which will be added and should not run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the ASM plugin with a post exec hook that will add a post exec hook. + // To ensure the ASM plugin's post exec hook runs first, it needs to be associated, so we also define an + // empty pre-exec hook. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: true, + setPostExec: true, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.POST_EXECUTION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // user op and runtime validation. This will trigger the ASM plugin's post exec hook to install the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // ASM plugin's hook should not run. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_postExec_add_postExec_notFirstElement() public { + // Source: post-Exec + // Target: post-Exec (same phase) + // Addition (non-first post-only): should *not* run + + // Set up the mock plugin with a post-Exec hook, which will be added and should not run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the ASM plugin with a post exec hook that will add a post exec hook. + // This will be a post-only hook. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: false, + setPostExec: true, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.POST_EXECUTION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // user op and runtime validation. This will trigger the ASM plugin's post exec hook to install the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // ASM plugin's hook should not run. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_postExec_remove_postExec_firstElement() public { + // Source: post-Exec + // Target: post-Exec (same phase) + // Removal (first post-only): should still run + + // Set up the mock plugin with a post-Exec hook, which will be removed and should still run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a post exec hook that will remove the mock plugin's post exec hook. + // To ensure the ASM plugin's post exec hook runs first, it needs to be associated, so we also define an + // empty pre-exec hook. This also ensures it is not the first post exec hook. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: true, + setPostExec: true, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.POST_EXECUTION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // user op and runtime validation. This will trigger the ASM plugin's post exec hook to remove the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // ASM plugin's hook should still run. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_postExec_remove_postExec_notFirstElement() public { + // Source: post-Exec + // Target: post-Exec (same phase) + // Removal (non-first post-only): should still run + + // Set up the mock plugin with a post-Exec hook, which will be removed and should still run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a post exec hook that will remove the mock plugin's post exec hook. + // This will be a post-only hook. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setPreExec: false, + setPostExec: true, + setRTValidation: false, + setPreRTValidation: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.POST_EXECUTION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account by mocking a call from the EntryPoint, bypassing + // user op and runtime validation. This will trigger the ASM plugin's post exec hook to remove the + // mock plugin's post exec hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should still run. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(address(entryPoint)); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } +} diff --git a/test/account/phases/AccountStatePhasesRTValidation.t.sol b/test/account/phases/AccountStatePhasesRTValidation.t.sol new file mode 100644 index 000000000..521ea0aa3 --- /dev/null +++ b/test/account/phases/AccountStatePhasesRTValidation.t.sol @@ -0,0 +1,1387 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +import {AccountStatePhasesTest} from "./AccountStatePhases.t.sol"; + +import {IPluginManager} from "../../../src/interfaces/IPluginManager.sol"; +import {IPlugin} from "../../../src/interfaces/IPlugin.sol"; +import {IStandardExecutor, Call} from "../../../src/interfaces/IStandardExecutor.sol"; +import {UpgradeableModularAccount} from "../../../src/account/UpgradeableModularAccount.sol"; + +import {AccountStateMutatingPlugin} from "../../mocks/plugins/AccountStateMutatingPlugin.sol"; + +// Tests the account state phase behavior when the source of the state modification +// happens during runtime validation. +contract AccountStatePhasesRTValidationTest is AccountStatePhasesTest { + // Test cases covered here: + // These are listed in the order they are run in the test suite. + // The "source" indicates which in which phase the plugin will perform a modification, and the "target" + // indicates which phase will change as a result of the modification. + // + // - Source: pre-Runtime-Validation + // - Target: pre-UserOp-Validation + // - n/a - can’t run in the same call + // - Target: UserOp-Validation + // - n/a - can’t run in the same call + // - Target: pre-Runtime-Validation (same phase) + // - Addition: adding a hook should not result in that hook running. + // - Removal: removing a hook should still have the hook run. + // - Target: Runtime-Validation (same phase) + // - Replace: original should run + // - Removal: original should run + // - Target: pre-Exec (different phase) + // - Addition (first element): should run + // - Addition (not first): should run + // - Removal: should *not* run + // - Target: Exec (different phase) + // - Replace: replacement should run + // - Removal: should revert as empty + // - Target: post-Exec (different phase) + // - Addition (associated, first pre-exec): should run + // - Addition (associated, non-first pre-exec): should run + // - Removal (associated, first pre-exec): should *not* run + // - Removal (associated, non-first pre-exec): should *not* run + // - Addition (first post-only): should run + // - Addition (non-first post-only): should run + // - Removal (first post-only): should *not* run + // - Removal (non-first post-only): should *not* run + // - Source: Runtime-Validation + // - Target: pre-UserOp-Validation + // - n/a - can’t run in the same call + // - Target: UserOp-Validation + // - n/a - can’t run in the same call + // - Target: pre-Runtime-Validation (same phase) + // - n/a - runs before + // - Target: Runtime-Validation (same phase) + // - Won’t test, since it’s the same single-element field. + // - Target: pre-Exec (different phase) + // - Addition (first element): should run + // - Addition (not first): should run + // - Removal: should *not* run + // - Target: Exec (different phase) + // - Replace: replacement should run + // - Removal: should revert as empty + // - Target: post-Exec (different phase) + // - Addition (associated, first pre-exec): should run + // - Addition (associated, non-first pre-exec): should run + // - Removal (associated, first pre-exec): should *not* run + // - Removal (associated, non-first pre-exec): should *not* run + // - Addition (first post-only): should run + // - Addition (non-first post-only): should run + // - Removal (first post-only): should *not* run + // - Removal (non-first post-only): should *not* run + + // Source: pre-Runtime-Validation + // Target: pre-UserOp-Validation + // n/a - can’t run in the same call + + // Source: pre-Runtime-Validation + // Target: UserOp-Validation + // n/a - can’t run in the same call + + function test_ASP_preRTValidation_add_preRTValidation() public { + // Source: pre-Runtime-Validation + // Target: pre-Runtime-Validation (same phase) + // Addition: adding a hook should not result in that hook running. + + // Set up the mock plugin with a pre-Runtime-Validation hook, which will be added and should not run. + _initMockPluginPreRuntimeValidationHook(); + + // Install the ASM plugin with a pre runtime validation hook that will add a pre runtime validation + // hook. + // Runtime validation is also needed to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: true, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.PRE_RUNTIME_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // ASM + // plugin's pre runtime validation function to install the mock plugin's pre runtime validation hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock + // plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(AccountStateMutatingPlugin.preRuntimeValidationHook.selector), + 0 // Should be called 0 times + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preRTValidation_remove_preRTValidation() public { + // Source: pre-Runtime-Validation + // Target: pre-Runtime-Validation (same phase) + // Removal: removing a hook should still have the hook run. + + // Set up the mock plugin with a pre-Runtime-Validation hook, which will be removed and should run. + _initMockPluginPreRuntimeValidationHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a pre runtime validation hook that will remove the mock plugin's pre + // runtime validation hook. + // Runtime validation is also needed to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: true, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_RUNTIME_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // ASM plugin's pre runtime validation function to remove the mock plugin's pre runtime validation hook. + // Per the 6900 spec, because this is in the same phase, the state change should not be applied and the + // mock plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(AccountStateMutatingPlugin.preRuntimeValidationHook.selector), + 1 // Should be called 1 time + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preRTValidation_replace_RTValidation() public { + // Source: pre-Runtime-Validation + // Target: Runtime-Validation (same phase) + // Replace: original should run + + // Set up the mock plugin with a Runtime-Validation function, which will replace the one defined by the + // ASM plugin and should not be run. + // To allow the call to complete as intended, we also add the execution function to the mock plugin. + m1.executionFunctions.push(AccountStateMutatingPlugin.executionFunction.selector); + _initMockPluginRuntimeValidationFunction(); + + // Install the ASM plugin with a pre runtime validation hook that will replace the runtime validation + // function. + // Runtime validation is also needed to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: true, + setPreExec: false, + setPostExec: false + }); + // Encode two self-calls: one to uninstall ASM plugin, one to install the mock plugin. + Call[] memory calls = _generateCallsUninstallASMInstallMock(); + asmPlugin.setCallback( + abi.encodeCall(IStandardExecutor.executeBatch, (calls)), + AccountStateMutatingPlugin.FunctionId.PRE_RUNTIME_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // ASM plugin's pre runtime validation function to replace the runtime validation function with the mock + // plugin's runtime validation function. The original should run, not the replacement. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(AccountStateMutatingPlugin.runtimeValidationFunction.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(AccountStateMutatingPlugin.runtimeValidationFunction.selector), + 0 // Should be called 0 times + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preRTValidation_remove_RTValidation() public { + // Source: pre-Runtime-Validation + // Target: Runtime-Validation (same phase) + // Removal: original should run + + // To allow the exec call to not revert, we add the execution function to the mock plugin. + _initMockPluginExecFunction(); + + // Install the ASM plugin with a pre runtime validation hook that will remove the runtime validation + // function. + // Runtime validation is also needed to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: true, + setPreExec: false, + setPostExec: false + }); + Call[] memory calls = _generateCallsUninstallASMInstallMock(); + asmPlugin.setCallback( + abi.encodeCall(IStandardExecutor.executeBatch, (calls)), + AccountStateMutatingPlugin.FunctionId.PRE_RUNTIME_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // ASM plugin's pre runtime validation function to remove the runtime validation function. The original + // runtime validation function should run. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(AccountStateMutatingPlugin.runtimeValidationFunction.selector), + 1 // Should be called 1 time + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preRTValidation_add_preExec_firstElement() public { + // Source: pre-Runtime-Validation + // Target: pre-Exec (different phase) + // Addition (first element): should run + + // Set up the mock plugin with a pre-Exec hook, which will be added and should run. + _initMockPluginPreExecutionHook(); + + // Install the ASM plugin with a pre runtime validation hook that will add a pre exec hook. + // It also needs a runtime validation function to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: true, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.PRE_RUNTIME_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // ASM plugin's pre runtime validation function to install the mock plugin's pre exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preRTValidation_add_preExec_notFirstElement() public { + // Source: pre-Runtime-Validation + // Target: pre-Exec (different phase) + // Addition (not first): should run + + // Set up the mock plugin with a pre-Exec hook, which will be added and should run. + _initMockPluginPreExecutionHook(); + + // Install the ASM plugin with a pre runtime validation hook that will add a pre exec hook. + // It also needs a runtime validation function to allow the call to be performed, and a pre exec hook to + // ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: true, + setPreExec: true, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.PRE_RUNTIME_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // ASM plugin's pre runtime validation function to install the mock plugin's pre exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preRTValidation_remove_preExec() public { + // Source: pre-Runtime-Validation + // Target: pre-Exec (different phase) + // Removal: should *not* run + + // Set up the mock plugin with a pre-Exec hook, which will be removed and should not run. + _initMockPluginPreExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a pre runtime validation hook that will remove the mock plugin's pre exec + // hook. + // It also needs a runtime validation function to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: true, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_RUNTIME_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // ASM plugin's pre runtime validation function to remove the mock plugin's pre exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preRTValidation_replace_exec() public { + // Source: pre-Runtime-Validation + // Target: Exec (different phase) + // Replace: replacement should run + + // Set up the mock plugin with an Exec function, which will replace the one defined by the ASM plugin + // and should be run. + _initMockPluginExecFunction(); + + // Install the ASM plugin with a pre runtime validation hook that will replace the exec function. + // Runtime validation is also needed to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: true, + setPreExec: false, + setPostExec: false + }); + // Encode two self-calls: one to uninstall ASM plugin, one to install the mock plugin. + Call[] memory calls = _generateCallsUninstallASMInstallMock(); + asmPlugin.setCallback( + abi.encodeCall(IStandardExecutor.executeBatch, (calls)), + AccountStateMutatingPlugin.FunctionId.PRE_RUNTIME_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // ASM plugin's pre runtime validation function to replace the exec function with the mock plugin's exec + // function. The replacement should run, not the original. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(AccountStateMutatingPlugin.executionFunction.selector), + 0 // Should be called 0 times + ); + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(AccountStateMutatingPlugin.executionFunction.selector), + 1 // Should be called 1 time + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preRTValidation_remove_exec() public { + // Source: pre-Runtime-Validation + // Target: Exec (different phase) + // Removal: should revert as empty + + // Install the ASM plugin with a pre runtime validation hook that will remove the exec function. + // Runtime validation is also needed to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: true, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(asmPlugin), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_RUNTIME_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // ASM plugin's pre runtime validation function to remove the exec function. This should cause the call to + // revert, but only after the ASM plugin's runtime validation function has run. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.runtimeValidationFunction.selector), + 1 // Should be called 1 time + ); + vm.expectRevert( + abi.encodeWithSelector( + UpgradeableModularAccount.UnrecognizedFunction.selector, + AccountStateMutatingPlugin.executionFunction.selector + ) + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preRTValidation_add_postExec_associated_firstElement() public { + // Source: pre-Runtime-Validation + // Target: post-Exec (different phase) + // Addition (associated, first pre-exec): should run + + // Set up the mock plugin with an associated post-Exec hook, which will be added and should run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the ASM plugin with a pre runtime validation hook that will add a post exec hook. + // It also needs a runtime validation function to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: true, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.PRE_RUNTIME_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // ASM plugin's pre runtime validation function to install the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preRTValidation_add_postExec_associated_notFirstElement() public { + // Source: pre-Runtime-Validation + // Target: post-Exec (different phase) + // Addition (associated, non-first pre-exec): should run + + // Set up the mock plugin with an associated post-Exec hook, which will be added and should run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the ASM plugin with a pre runtime validation hook that will add a post exec hook. + // It also needs a runtime validation function to allow the call to be performed, and a pre exec hook to + // ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: true, + setPreExec: true, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.PRE_RUNTIME_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // ASM plugin's pre runtime validation function to install the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preRTValidation_remove_postExec_associated_firstElement() public { + // Source: pre-Runtime-Validation + // Target: post-Exec (different phase) + // Removal (associated, first pre-exec): should *not* run + + // Set up the mock plugin with an associated post-Exec hook, which will be removed and should not run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a pre runtime validation hook that will remove the mock plugin's post exec + // hook. + // It also needs a runtime validation function to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: true, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_RUNTIME_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // ASM plugin's pre runtime validation function to remove the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preRTValidation_remove_postExec_associated_notFirstElement() public { + // Source: pre-Runtime-Validation + // Target: post-Exec (different phase) + // Removal (associated, non-first pre-exec): should *not* run + + // Set up the mock plugin with an associated post-Exec hook, which will be removed and should not run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a pre runtime validation hook that will remove the mock plugin's post exec + // hook. It also needs a runtime validation function to allow the call to be performed, and a pre exec + // hook to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: true, + setPreExec: true, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_RUNTIME_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // ASM plugin's pre runtime validation function to remove the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preRTValidation_add_postExec_firstElement() public { + // Source: pre-Runtime-Validation + // Target: post-Exec (different phase) + // Addition (first post-only): should run + + // Set up the mock plugin with a post-Exec hook, which will be added and should run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the ASM plugin with a pre runtime validation hook that will add a post exec hook. + // It also needs a runtime validation function to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: true, + setPostExec: false, + setPreExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.PRE_RUNTIME_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // ASM plugin's pre runtime validation function to install the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preRTValidation_add_postExec_notFirstElement() public { + // Source: pre-Runtime-Validation + // Target: post-Exec (different phase) + // Addition (non-first post-only): should run + + // Set up the mock plugin with a post-Exec hook, which will be added and should run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the ASM plugin with a pre runtime validation hook that will add a post exec hook. + // It also needs a runtime validation function to allow the call to be performed, and a post-only exec hook + // to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: true, + setPostExec: true, + setPreExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.PRE_RUNTIME_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // ASM plugin's pre runtime validation function to install the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preRTValidation_remove_postExec_firstElement() public { + // Source: pre-Runtime-Validation + // Target: post-Exec (different phase) + // Removal (first post-only): should *not* run + + // Set up the mock plugin with a post-Exec hook, which will be removed and should not run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a pre runtime validation hook that will remove the mock plugin's post exec + // hook. + // It also needs a runtime validation function to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: true, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_RUNTIME_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // ASM plugin's pre runtime validation function to remove the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_preRTValidation_remove_postExec_notFirstElement() public { + // Source: pre-Runtime-Validation + // Target: post-Exec (different phase) + // Removal (non-first post-only): should *not* run + + // Set up the mock plugin with a post-Exec hook, which will be removed and should not run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a pre runtime validation hook that will remove the mock plugin's post exec + // hook. It also needs a runtime validation function to allow the call to be performed, and a post-only + // exec hook to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: true, + setPostExec: true, + setPreExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_RUNTIME_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // ASM plugin's pre runtime validation function to remove the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + // Source: Runtime-Validation + // Target: pre-UserOp-Validation + // n/a - can’t run in the same call + + // Source: Runtime-Validation + // Target: UserOp-Validation + // n/a - can’t run in the same call + + // Source: Runtime-Validation + // Target: pre-Runtime-Validation (same phase) + // n/a - runs before + + // Source: Runtime-Validation + // Target: Runtime-Validation (same phase) + // Won’t test, since it’s the same single-element field. + + function test_ASP_RTValidation_add_preExec_firstElement() public { + // Source: Runtime-Validation + // Target: pre-Exec (different phase) + // Addition (first element): should run + + // Set up the mock plugin with a pre-Exec hook, which will be added and should run. + _initMockPluginPreExecutionHook(); + + // Install the ASM plugin with a runtime validation function that will add a pre exec hook. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.RUNTIME_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // mock plugin's runtime validation function to install the mock plugin's pre exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_RTValidation_add_preExec_notFirstElement() public { + // Source: Runtime-Validation + // Target: pre-Exec (different phase) + // Addition (not first): should run + + // Set up the mock plugin with a pre-Exec hook, which will be added and should run. + _initMockPluginPreExecutionHook(); + + // Install the ASM plugin with a runtime validation function that will add a pre exec hook. + // It also needs a pre exec hook to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: false, + setPreExec: true, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.RUNTIME_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // mock plugin's runtime validation function to install the mock plugin's pre exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_RTValidation_remove_preExec() public { + // Source: Runtime-Validation + // Target: pre-Exec (different phase) + // Removal: should *not* run + + // Set up the mock plugin with a pre-Exec hook, which will be removed and should not run. + _initMockPluginPreExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a runtime validation function that will remove the mock plugin's pre exec + // hook. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.RUNTIME_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger the + // mock plugin's runtime validation function to remove the mock plugin's pre exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_RTValidation_replace_exec() public { + // Source: Runtime-Validation + // Target: Exec (different phase) + // Replace: replacement should run + + // Set up the mock plugin with an Exec function, which will replace the one defined by the ASM plugin + // and should be run. + _initMockPluginExecFunction(); + + // Install the ASM plugin with a runtime validation function that will replace the exec function. + // Runtime validation is also needed to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + // Encode two self-calls: one to uninstall ASM plugin, one to install the mock plugin. + Call[] memory calls = _generateCallsUninstallASMInstallMock(); + asmPlugin.setCallback( + abi.encodeCall(IStandardExecutor.executeBatch, (calls)), + AccountStateMutatingPlugin.FunctionId.RUNTIME_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger + // the ASM plugin's runtime validation function to replace the exec function with the mock plugin's exec + // function. The replacement should run, not the original. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(AccountStateMutatingPlugin.executionFunction.selector), + 0 // Should be called 0 times + ); + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(AccountStateMutatingPlugin.executionFunction.selector), + 1 // Should be called 1 time + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_RTValidation_remove_exec() public { + // Source: Runtime-Validation + // Target: Exec (different phase) + // Removal: should revert as empty + + // Install the ASM plugin with a runtime validation function that will remove the exec function. + // Runtime validation is also needed to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(asmPlugin), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.RUNTIME_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger + // the ASM plugin's runtime validation function to remove the exec function. This should cause the call to + // revert, but only after the ASM plugin's runtime validation function has run. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.runtimeValidationFunction.selector), + 1 // Should be called 1 time + ); + vm.expectRevert( + abi.encodeWithSelector( + UpgradeableModularAccount.UnrecognizedFunction.selector, + AccountStateMutatingPlugin.executionFunction.selector + ) + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_RTValidation_add_postExec_associated_firstElement() public { + // Source: Runtime-Validation + // Target: post-Exec (different phase) + // Addition (associated, first pre-exec): should run + + // Set up the mock plugin with an associated post-Exec hook, which will be added and should run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the ASM plugin with a runtime validation function that will add a post exec hook. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.RUNTIME_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger + // the ASM plugin's runtime validation function to install the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_RTValidation_add_postExec_associated_notFirstElement() public { + // Source: Runtime-Validation + // Target: post-Exec (different phase) + // Addition (associated, non-first pre-exec): should run + + // Set up the mock plugin with an associated post-Exec hook, which will be added and should run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the ASM plugin with a runtime validation function that will add a post exec hook. + // It also needs a pre exec hook to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: false, + setPreExec: true, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.RUNTIME_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger + // the ASM plugin's runtime validation function to install the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_RTValidation_remove_postExec_associated_firstElement() public { + // Source: Runtime-Validation + // Target: post-Exec (different phase) + // Removal (associated, first pre-exec): should *not* run + + // Set up the mock plugin with an associated post-Exec hook, which will be removed and should not run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a runtime validation function that will remove the mock plugin's post exec + // hook. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.RUNTIME_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger + // the ASM plugin's runtime validation function to remove the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_RTValidation_remove_postExec_associated_notFirstElement() public { + // Source: Runtime-Validation + // Target: post-Exec (different phase) + // Removal (associated, non-first pre-exec): should *not* run + + // Set up the mock plugin with an associated post-Exec hook, which will be removed and should not run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a runtime validation function that will remove the mock plugin's post exec + // hook. It also needs a pre exec hook to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: false, + setPreExec: true, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.RUNTIME_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger + // the ASM plugin's runtime validation function to remove the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_RTValidation_add_postExec_firstElement() public { + // Source: Runtime-Validation + // Target: post-Exec (different phase) + // Addition (first post-only): should run + + // Set up the mock plugin with a post-Exec hook, which will be added and should run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the ASM plugin with a runtime validation function that will add a post exec hook. + // It also needs a runtime validation function to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: false, + setPostExec: false, + setPreExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.RUNTIME_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger + // the ASM plugin's runtime validation function to install the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_RTValidation_add_postExec_notFirstElement() public { + // Source: Runtime-Validation + // Target: post-Exec (different phase) + // Addition (non-first post-only): should run + + // Set up the mock plugin with a post-Exec hook, which will be added and should run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the ASM plugin with a runtime validation function that will add a post exec hook. + // It also needs a post-only exec hook to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: false, + setPostExec: true, + setPreExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.RUNTIME_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger + // the ASM plugin's runtime validation function to install the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_RTValidation_remove_postExec_firstElement() public { + // Source: Runtime-Validation + // Target: post-Exec (different phase) + // Removal (first post-only): should *not* run + + // Set up the mock plugin with a post-Exec hook, which will be removed and should not run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a runtime validation function that will remove the mock plugin's post exec + // hook. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: false, + setPostExec: false, + setPreExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.RUNTIME_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger + // the ASM plugin's runtime validation function to remove the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } + + function test_ASP_RTValidation_remove_postExec_notFirstElement() public { + // Source: Runtime-Validation + // Target: post-Exec (different phase) + // Removal (non-first post-only): should *not* run + + // Set up the mock plugin with a post-Exec hook, which will be removed and should not run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a runtime validation function that will remove the mock plugin's post exec + // hook. It also needs a post-only exec hook to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: false, + setPreUOValidation: false, + setRTValidation: true, + setPreRTValidation: false, + setPostExec: true, + setPreExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.RUNTIME_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a direct runtime call. This will trigger + // the ASM plugin's runtime validation function to remove the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the + // mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.prank(owner1); + AccountStateMutatingPlugin(address(account1)).executionFunction(); + } +} diff --git a/test/account/phases/AccountStatePhasesUOValidation.t.sol b/test/account/phases/AccountStatePhasesUOValidation.t.sol new file mode 100644 index 000000000..a4851b9c5 --- /dev/null +++ b/test/account/phases/AccountStatePhasesUOValidation.t.sol @@ -0,0 +1,1341 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +import {AccountStatePhasesTest} from "./AccountStatePhases.t.sol"; + +import {IPluginManager} from "../../../src/interfaces/IPluginManager.sol"; +import {IPlugin} from "../../../src/interfaces/IPlugin.sol"; +import {IStandardExecutor, Call} from "../../../src/interfaces/IStandardExecutor.sol"; + +import {AccountStateMutatingPlugin} from "../../mocks/plugins/AccountStateMutatingPlugin.sol"; + +// Tests the account state phase behavior when the source of the state modification +// happens during user op validation. +contract AccountStatePhasesUOValidationTest is AccountStatePhasesTest { + // Test cases covered here: + // These are listed in the order they are run in the test suite. + // The "source" indicates which in which phase the plugin will perform a modification, and the "target" + // indicates which phase will change as a result of the modification. + // + // - Source: pre-UserOp-Validation + // - Target: pre-UserOp-Validation (same phase) + // - Addition: adding a hook should not result in that hook running. + // - Removal: removing a hook should still have the hook run. + // - Target: UserOp-Validation (same phase) + // - Replace: original should run + // - Removal: original should run + // - Target: pre-Runtime-Validation (different phase) + // - n/a - can’t run in the same user op + // - Target: Runtime-Validation (different phase) + // - n/a - can’t run in the same user op + // - Target: pre-Exec (different phase) + // - Addition (first element): should run + // - Addition (not first): should run + // - Removal: should *not* run + // - Target: Exec (different phase) + // - Replace: replacement should run + // - Removal: should revert as empty + // - Target: post-Exec (different phase) + // - Addition (associated, first pre-exec): should run + // - Addition (associated, non-first pre-exec): should run + // - Removal (associated, first pre-exec): should *not* run + // - Removal (associated, non-first pre-exec): should *not* run + // - Addition (first post-only): should run + // - Addition (non-first post-only): should run + // - Removal (first post-only): should *not* run + // - Removal (non-first post-only): should *not* run + // - Source: UserOp-Validation + // - Target: pre-UserOp-Validation (same phase) + // - n/a - happens before user op validation + // - Target: UserOp-Validation (same phase) + // - Won’t test, since it’s the same single-element field. + // - Target: pre-Runtime-Validation (different phase) + // - n/a - can’t run in the same user op + // - Target: Runtime-Validation (different phase) + // - n/a - can’t run in the same user op + // - Target: pre-Exec (different phase) + // - Addition (first element): should run + // - Addition (not first): should run + // - Removal: should *not* run + // - Target: Exec (different phase) + // - Replace: replacement should run + // - Removal: should revert as empty + // - Target: post-Exec (different phase) + // - Addition (associated, first pre-exec): should run + // - Addition (associated, non-first pre-exec): should run + // - Removal (associated, first pre-exec): should *not* run + // - Removal (associated, non-first pre-exec): should *not* run + // - Addition (first post-only): should run + // - Addition (non-first post-only): should run + // - Removal (first post-only): should *not* run + // - Removal (non-first post-only): should *not* run + + function test_ASP_preUOValidation_add_preUOValidation() public { + // Source: pre-UserOp-Validation + // Target: pre-UserOp-Validation (same phase) + // Addition: adding a hook should not result in that hook running. + + // Set up the mock plugin with a pre-UserOp-Validation hook, which will be added and should not run. + _initMockPluginPreUserOpValidationHook(); + + // Install the ASM plugin with a pre user op validation hook that will add a pre user op validation hook. + // It also needs a user op validation function to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: true, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.PRE_USER_OP_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin to + // install the mock plugin's pre user op validation hook during the first pre-UserOp-Validation hook. + // Per the 6900 spec, the state change should not be applied yet and the mock plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preUserOpValidationHook.selector), + 0 // Should be called 0 times + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_preUOValidation_remove_preUOValidation() public { + // Source: pre-UserOp-Validation + // Target: pre-UserOp-Validation (same phase) + // Removal: removing a hook should still have the hook run. + + // Set up the mock plugin with a pre-UserOp-Validation hook, which will be removed and should still run. + _initMockPluginPreUserOpValidationHook(); + + // Install the plugin as part of the starting state. By installing this first, it will run AFTER the ASM + // plugin's pre-UserOp-Validation hook, giving the modification a chance to change the logic. + _installMockPlugin(); + + // Install the ASM plugin with a pre user op validation hook that will remove the mock plugin's pre user op + // validation hook. + // It also needs a user op validation function to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: true, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_USER_OP_VALIDATION_HOOK + ); + _installASMPlugin(); + + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preUserOpValidationHook.selector), + 1 // Should be called 1 time + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_preUOValidation_replace_UOValidation() public { + // Source: pre-UserOp-Validation + // Target: UserOp-Validation (same phase) + // Replace: original should run + + // Set up the mock plugin with a userOpValidation function, which will replace the one defined by the ASM + // plugin and should not be run. + _initMockPluginUserOpValidationFunction(); + + // Install the ASM plugin with a pre user op validation hook that will replace its own user op validation + // function. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: true, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + // Encode two self-calls: one to uninstall ASM plugin, one to install the mock plugin. + Call[] memory calls = _generateCallsUninstallASMInstallMock(); + asmPlugin.setCallback( + abi.encodeCall(IStandardExecutor.executeBatch, (calls)), + AccountStateMutatingPlugin.FunctionId.PRE_USER_OP_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin to + // replace its own user op validation function with the mock plugin's user op validation function during + // the + // first pre-UserOp-Validation hook. The original should run, not the replacement. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.userOpValidationFunction.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.userOpValidationFunction.selector), + 0 // Should be called 0 times + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_preUOValidation_remove_UOValidation() public { + // Source: pre-UserOp-Validation + // Target: UserOp-Validation (same phase) + // Removal: original should run + + // Install the ASM plugin with a pre user op validation hook that will remove its own user op validation + // function. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: true, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(asmPlugin), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_USER_OP_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin to + // remove its own user op validation function during the first pre-UserOp-Validation hook. The original + // should run. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.userOpValidationFunction.selector), + 1 // Should be called 1 time + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + // Source: pre-UserOp-Validation + // Target: pre-Runtime-Validation (different phase) + // n/a - can’t run in the same user op + + // Source: pre-UserOp-Validation + // Target: Runtime-Validation (different phase) + // n/a - can’t run in the same user op + + function test_ASP_preUOValidation_add_preExec_firstElement() public { + // Source: pre-UserOp-Validation + // Target: pre-Exec (different phase) + // Addition (first element): should run + + // Set up the mock plugin with a pre-Exec hook, which will be added and should run. + _initMockPluginPreExecutionHook(); + + // Install the ASM plugin with a pre user op validation hook that will add a pre exec hook. + // It also needs a user op validation function to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: true, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.PRE_USER_OP_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin to + // install the mock plugin's pre exec hook during the first pre-UserOp-Validation hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preExecutionHook.selector), + 1 // Should be called 1 time + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_preUOValidation_add_preExec_notFirstElement() public { + // Source: pre-UserOp-Validation + // Target: pre-Exec (different phase) + // Addition (not first): should run + + // Set up the mock plugin with a pre-Exec hook, which will be added and should run. + _initMockPluginPreExecutionHook(); + + // Install the ASM plugin with a pre user op validation hook that will add a pre exec hook. + // It also needs a user op validation function to allow the call to be performed, and a pre exec hook to + // ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: true, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: true, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.PRE_USER_OP_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin to + // install the mock plugin's pre exec hook during the first pre-UserOp-Validation hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preExecutionHook.selector), + 1 // Should be called 1 time + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_preUOValidation_remove_preExec() public { + // Source: pre-UserOp-Validation + // Target: pre-Exec (different phase) + // Removal: should *not* run + + // Set up the mock plugin with a pre-Exec hook, which will be removed and should not run. + _initMockPluginPreExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a pre user op validation hook that will remove the mock plugin's pre exec + // hook. + // It also needs a user op validation function to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: true, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_USER_OP_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin to + // remove the mock plugin's pre exec hook during the first pre-UserOp-Validation hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preExecutionHook.selector), + 0 // Should be called 0 times + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_preUOValidation_replace_exec() public { + // Source: pre-UserOp-Validation + // Target: Exec (different phase) + // Replace: replacement should run + + // Set up the mock plugin with an exec function, which will replace the one defined by the ASM plugin and + // should be run. + _initMockPluginExecFunction(); + + // Install the ASM plugin with a pre user op validation hook that will replace the exec function. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: true, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + // Encode two self-calls: one to uninstall ASM plugin, one to install the mock plugin. + Call[] memory calls = _generateCallsUninstallASMInstallMock(); + asmPlugin.setCallback( + abi.encodeCall(IStandardExecutor.executeBatch, (calls)), + AccountStateMutatingPlugin.FunctionId.PRE_USER_OP_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin to + // replace its own exec function with the mock plugin's exec function during the first + // pre-UserOp-Validation + // hook. The replacement should run, not the original. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(AccountStateMutatingPlugin.executionFunction.selector), + 0 // Should be called 0 times + ); + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(AccountStateMutatingPlugin.executionFunction.selector), + 1 // Should be called 1 time + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_preUOValidation_remove_exec() public { + // Source: pre-UserOp-Validation + // Target: Exec (different phase) + // Removal: should revert as empty + + // Install the ASM plugin with a pre user op validation hook that will remove the exec function. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: true, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(asmPlugin), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_USER_OP_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin to + // remove its own exec function during the first pre-UserOp-Validation hook. Then, the call should revert + // during the execution phase because the exec function is empty. + + // Cannot use vm.expectRevert because it would only apply to the top-level call to `handleOps`, not the + // internal call. Instead, we use expectCall with a count of 0. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(AccountStateMutatingPlugin.executionFunction.selector), + 0 // Should be called 0 times + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_preUOValidation_add_postExec_associated_firstElement() public { + // Source: pre-UserOp-Validation + // Target: post-Exec (different phase) + // Addition (associated, first pre-exec): should run + + // Set up the mock plugin with an associated post-Exec hook, which will be added and should run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the ASM plugin with a pre user op validation hook that will add a post exec hook. + // It also needs a user op validation function to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: true, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.PRE_USER_OP_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin to + // install the mock plugin's associated post exec hook during the first pre-UserOp-Validation hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_preUOValidation_add_postExec_associated_notFirstElement() public { + // Source: pre-UserOp-Validation + // Target: post-Exec (different phase) + // Addition (associated, non-first pre-exec): should run + + // Set up the mock plugin with an associated post-Exec hook, which will be added and should run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the ASM plugin with a pre user op validation hook that will add a post exec hook. + // It also needs a user op validation function to allow the call to be performed, and a pre exec hook to + // ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: true, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: true, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.PRE_USER_OP_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin to + // install the mock plugin's associated post exec hook during the first pre-UserOp-Validation hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_preUOValidation_remove_postExec_associated_firstElement() public { + // Source: pre-UserOp-Validation + // Target: post-Exec (different phase) + // Removal (associated, first pre-exec): should *not* run + + // Set up the mock plugin with an associated post-Exec hook, which will be removed and should not run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a pre user op validation hook that will remove the mock plugin's associated + // post exec hook. + // It also needs a user op validation function to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: true, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_USER_OP_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin to + // remove the mock plugin's associated post exec hook during the first pre-UserOp-Validation hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_preUOValidation_remove_postExec_associated_notFirstElement() public { + // Source: pre-UserOp-Validation + // Target: post-Exec (different phase) + // Removal (associated, non-first pre-exec): should *not* run + + // Set up the mock plugin with an associated post-Exec hook, which will be removed and should not run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a pre user op validation hook that will remove the mock plugin's associated + // post exec hook. + // It also needs a user op validation function to allow the call to be performed, and a pre exec hook to + // ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: true, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: true, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_USER_OP_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin to + // remove the mock plugin's associated post exec hook during the first pre-UserOp-Validation hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_preUOValidation_add_postExec_firstElement() public { + // Source: pre-UserOp-Validation + // Target: post-Exec (different phase) + // Addition (first post-only): should run + + // Set up the mock plugin with a post-Exec hook, which will be added and should run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the ASM plugin with a pre user op validation hook that will add a post exec hook. + // It also needs a user op validation function to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: true, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.PRE_USER_OP_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin to + // install the mock plugin's post exec hook during the first pre-UserOp-Validation hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_preUOValidation_add_postExec_notFirstElement() public { + // Source: pre-UserOp-Validation + // Target: post-Exec (different phase) + // Addition (non-first post-only): should run + + // Set up the mock plugin with a post-Exec hook, which will be added and should run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the ASM plugin with a pre user op validation hook that will add a post exec hook. + // It also needs a user op validation function to allow the call to be performed, and a pre exec hook to + // ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: true, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: true + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.PRE_USER_OP_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin to + // install the mock plugin's post exec hook during the first pre-UserOp-Validation hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_preUOValidation_remove_postExec_firstElement() public { + // Source: pre-UserOp-Validation + // Target: post-Exec (different phase) + // Removal (first post-only): should *not* run + + // Set up the mock plugin with a post-Exec hook, which will be removed and should not run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a pre user op validation hook that will remove the mock plugin's post exec + // hook. + // It also needs a user op validation function to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: true, + setRTValidation: false, + setPreRTValidation: false, + setPostExec: false, + setPreExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_USER_OP_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin to + // remove the mock plugin's post exec hook during the first pre-UserOp-Validation hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_preUOValidation_remove_postExec_notFirstElement() public { + // Source: pre-UserOp-Validation + // Target: post-Exec (different phase) + // Removal (non-first post-only): should *not* run + + // Set up the mock plugin with a post-Exec hook, which will be removed and should not run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a pre user op validation hook that will remove the mock plugin's post exec + // hook. + // It also needs a user op validation function to allow the call to be performed, and a post-only exec hook + // to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: true, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: true + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.PRE_USER_OP_VALIDATION_HOOK + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin to + // remove the mock plugin's post exec hook during the first pre-UserOp-Validation hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + // Source: UserOp-Validation + // Target: pre-UserOp-Validation (same phase) + // n/a - happens before user op validation + + // Source: UserOp-Validation + // Target: UserOp-Validation (same phase) + // Won’t test, since it’s the same single-element field. + + // Source: UserOp-Validation + // Target: pre-Runtime-Validation (different phase) + // n/a - can’t run in the same user op + + // Source: UserOp-Validation + // Target: Runtime-Validation (different phase) + // n/a - can’t run in the same user op + + function test_ASP_UOValidation_add_preExec_firstElement() public { + // Source: UserOp-Validation + // Target: pre-Exec (different phase) + // Addition (first element): should run + + // Set up the mock plugin with a pre-Exec hook, which will be added and should run. + _initMockPluginPreExecutionHook(); + + // Install the ASM plugin with a user op validation function that will add a pre exec hook. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: false, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.USER_OP_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin's + // user op validation function to install the mock plugin's pre exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preExecutionHook.selector), + 1 // Should be called 1 time + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_UOValidation_add_preExec_notFirstElement() public { + // Source: UserOp-Validation + // Target: pre-Exec (different phase) + // Addition (not first): should run + + // Set up the mock plugin with a pre-Exec hook, which will be added and should run. + _initMockPluginPreExecutionHook(); + + // Install the ASM plugin with a user op validation function that will add a pre exec hook. + // It also needs a pre exec hook to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: false, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: true, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.USER_OP_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin's + // user op validation function to install the mock plugin's pre exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preExecutionHook.selector), + 1 // Should be called 1 time + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_UOValidation_remove_preExec() public { + // Source: UserOp-Validation + // Target: pre-Exec (different phase) + // Removal: should *not* run + + // Set up the mock plugin with a pre-Exec hook, which will be removed and should not run. + _initMockPluginPreExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a user op validation function that will remove the mock plugin's pre exec + // hook. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: false, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.USER_OP_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin's + // user op validation function to remove the mock plugin's pre exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.preExecutionHook.selector), + 0 // Should be called 0 times + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_UOValidation_replace_exec() public { + // Source: UserOp-Validation + // Target: Exec (different phase) + // Replace: replacement should run + + // Set up the mock plugin with an exec function, which will replace the one defined by the ASM plugin and + // should be run. + _initMockPluginExecFunction(); + + // Install the ASM plugin with a user op validation function that will replace the exec function. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: false, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + // Encode two self-calls: one to uninstall ASM plugin, one to install the mock plugin. + Call[] memory calls = _generateCallsUninstallASMInstallMock(); + asmPlugin.setCallback( + abi.encodeCall(IStandardExecutor.executeBatch, (calls)), + AccountStateMutatingPlugin.FunctionId.USER_OP_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin's + // user op validation function to replace the exec function with the mock plugin's exec function. The + // replacement should run, not the original. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(AccountStateMutatingPlugin.executionFunction.selector), + 0 // Should be called 0 times + ); + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(AccountStateMutatingPlugin.executionFunction.selector), + 1 // Should be called 1 time + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_UOValidation_remove_exec() public { + // Source: UserOp-Validation + // Target: Exec (different phase) + // Removal: should revert as empty + + // Install the ASM plugin with a user op validation function that will remove the exec function. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: false, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(asmPlugin), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.USER_OP_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin's + // user op validation function to remove the exec function. Then, the call should revert during the + // execution phase because the exec function is empty. + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(AccountStateMutatingPlugin.executionFunction.selector), + 0 // Should be called 0 times + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_UOValidation_add_postExec_associated_firstElement() public { + // Source: UserOp-Validation + // Target: post-Exec (different phase) + // Addition (associated, first pre-exec): should run + + // Set up the mock plugin with an associated post-Exec hook, which will be added and should run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the ASM plugin with a user op validation function that will add a post exec hook. + // It also needs a user op validation function to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: false, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.USER_OP_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin's + // user op validation function to install the mock plugin's associated post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_UOValidation_add_postExec_associated_notFirstElement() public { + // Source: UserOp-Validation + // Target: post-Exec (different phase) + // Addition (associated, non-first pre-exec): should run + + // Set up the mock plugin with an associated post-Exec hook, which will be added and should run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the ASM plugin with a user op validation function that will add a post exec hook. + // It also needs a user op validation function to allow the call to be performed, and a pre exec hook to + // ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: false, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: true, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.USER_OP_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin's + // user op validation function to install the mock plugin's associated post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_UOValidation_remove_postExec_associated_firstElement() public { + // Source: UserOp-Validation + // Target: post-Exec (different phase) + // Removal (associated, first pre-exec): should *not* run + + // Set up the mock plugin with an associated post-Exec hook, which will be removed and should not run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a user op validation function that will remove the mock plugin's associated + // post exec hook. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: false, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.USER_OP_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin's + // user op validation function to remove the mock plugin's associated post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_UOValidation_remove_postExec_associated_notFirstElement() public { + // Source: UserOp-Validation + // Target: post-Exec (different phase) + // Removal (associated, non-first pre-exec): should *not* run + + // Set up the mock plugin with an associated post-Exec hook, which will be removed and should not run. + _initMockPluginPreAndPostExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a user op validation function that will remove the mock plugin's associated + // post exec hook. + // It also needs a user op validation function to allow the call to be performed, and a pre exec hook to + // ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: false, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: true, + setPostExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.USER_OP_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin's + // user op validation function to remove the mock plugin's associated post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_UOValidation_add_postExec_firstElement() public { + // Source: UserOp-Validation + // Target: post-Exec (different phase) + // Addition (first post-only): should run + + // Set up the mock plugin with a post-Exec hook, which will be added and should run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the ASM plugin with a user op validation function that will add a post exec hook. + // It also needs a user op validation function to allow the call to be performed. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: false, + setRTValidation: false, + setPreRTValidation: false, + setPostExec: false, + setPreExec: false + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.USER_OP_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin's + // user op validation function to install the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_UOValidation_add_postExec_notFirstElement() public { + // Source: UserOp-Validation + // Target: post-Exec (different phase) + // Addition (non-first post-only): should run + + // Set up the mock plugin with a post-Exec hook, which will be added and should run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the ASM plugin with a user op validation function that will add a post exec hook. + // It also needs a user op validation function to allow the call to be performed, and a post-only exec hook + // to + // ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: false, + setRTValidation: false, + setPreRTValidation: false, + setPreExec: false, + setPostExec: true + }); + asmPlugin.setCallback( + abi.encodeCall( + IPluginManager.installPlugin, + (address(mockPlugin1), manifestHash1, "", _EMPTY_DEPENDENCIES, _EMPTY_INJECTED_HOOKS) + ), + AccountStateMutatingPlugin.FunctionId.USER_OP_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin's + // user op validation function to install the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_UOValidation_remove_postExec_firstElement() public { + // Source: UserOp-Validation + // Target: post-Exec (different phase) + // Removal (first post-only): should *not* run + + // Set up the mock plugin with a post-Exec hook, which will be removed and should not run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a user op validation function that will remove the mock plugin's post exec + // hook. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: false, + setRTValidation: false, + setPreRTValidation: false, + setPostExec: false, + setPreExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.USER_OP_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin's + // user op validation function to remove the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } + + function test_ASP_UOValidation_remove_postExec_notFirstElement() public { + // Source: UserOp-Validation + // Target: post-Exec (different phase) + // Removal (non-first post-only): should *not* run + + // Set up the mock plugin with a post-Exec hook, which will be removed and should not run. + _initMockPluginPostOnlyExecutionHook(); + + // Install the mock plugin as part of the starting state. + _installMockPlugin(); + + // Install the ASM plugin with a user op validation function that will remove the mock plugin's post exec + // hook. + // It also needs a user op validation function to allow the call to be performed, and a post-only exec hook + // to ensure that the mock plugin's hook is not the first one. + asmPlugin.configureInstall({ + setUOValidation: true, + setPreUOValidation: false, + setRTValidation: false, + setPreRTValidation: false, + setPostExec: true, + setPreExec: false + }); + asmPlugin.setCallback( + abi.encodeCall(IPluginManager.uninstallPlugin, (address(mockPlugin1), "", "", _EMPTY_HOOK_APPLY_DATA)), + AccountStateMutatingPlugin.FunctionId.USER_OP_VALIDATION + ); + _installASMPlugin(); + + // Call the `executionFunction` function on the account via a user op. This will trigger the ASM plugin's + // user op validation function to remove the mock plugin's post exec hook. + // Per the 6900 spec, because this is in a different phase, the state change should be applied and the mock + // plugin's hook should not run. + vm.expectCall( + address(mockPlugin1), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 0 // Should be called 0 times + ); + vm.expectCall( + address(asmPlugin), + // Partial calldata is provided to match against different parameters. + abi.encodeWithSelector(IPlugin.postExecutionHook.selector), + 1 // Should be called 1 time + ); + entryPoint.handleOps(_generateAndSignUserOp(), beneficiary); + } +} diff --git a/test/mocks/plugins/AccountStateMutatingPlugin.sol b/test/mocks/plugins/AccountStateMutatingPlugin.sol new file mode 100644 index 000000000..cad126030 --- /dev/null +++ b/test/mocks/plugins/AccountStateMutatingPlugin.sol @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +import { + PluginManifest, + ManifestExecutionHook, + ManifestFunction, + ManifestAssociatedFunctionType, + ManifestAssociatedFunction +} from "../../../src/interfaces/IPlugin.sol"; +import {UserOperation} from "../../../src/interfaces/erc4337/UserOperation.sol"; + +import {BaseTestPlugin} from "./BaseTestPlugin.sol"; + +// Used in conjunction with AccountStatePhasesTest to verify that the account state is consistent when plugins are +// updated mid-execution. +contract AccountStateMutatingPlugin is BaseTestPlugin { + enum FunctionId { + PRE_USER_OP_VALIDATION_HOOK, + USER_OP_VALIDATION, + PRE_RUNTIME_VALIDATION_HOOK, + RUNTIME_VALIDATION, + PRE_EXECUTION_HOOK, + EXECUTION_FUNCTION, // Not actually used as a function id in the manifest, just makes it easier to write + // the callback setter + POST_EXECUTION_HOOK + } + + bool hasUOValidation; + bool hasPreUOValidation; + bool hasRTValidation; + bool hasPreRTValidation; + bool hasPreExec; + bool hasPostExec; + + bytes UOValidationCallback; + bytes preUOValidationCallback; + bytes RTValidationCallback; + bytes preRTValidationCallback; + bytes preExecCallback; + bytes execCallback; + bytes postExecCallback; + + // Specify what functions should be added when this is installed. + function configureInstall( + bool setUOValidation, + bool setPreUOValidation, + bool setRTValidation, + bool setPreRTValidation, + bool setPreExec, + bool setPostExec + ) public { + hasUOValidation = setUOValidation; + hasPreUOValidation = setPreUOValidation; + hasRTValidation = setRTValidation; + hasPreRTValidation = setPreRTValidation; + hasPreExec = setPreExec; + hasPostExec = setPostExec; + } + + function setCallback(bytes calldata callback, FunctionId where) external { + if (where == FunctionId.PRE_USER_OP_VALIDATION_HOOK) { + preUOValidationCallback = callback; + } else if (where == FunctionId.USER_OP_VALIDATION) { + UOValidationCallback = callback; + } else if (where == FunctionId.PRE_RUNTIME_VALIDATION_HOOK) { + preRTValidationCallback = callback; + } else if (where == FunctionId.RUNTIME_VALIDATION) { + RTValidationCallback = callback; + } else if (where == FunctionId.PRE_EXECUTION_HOOK) { + preExecCallback = callback; + } else if (where == FunctionId.EXECUTION_FUNCTION) { + execCallback = callback; + } else if (where == FunctionId.POST_EXECUTION_HOOK) { + postExecCallback = callback; + } else { + revert NotImplemented(); + } + } + + function pluginManifest() external pure override returns (PluginManifest memory) { + return _castToPure(_getManifest)(); + } + + function _castToPure(function() internal view returns (PluginManifest memory) fnIn) + internal + pure + returns (function() internal pure returns (PluginManifest memory) fnOut) + { + assembly { + fnOut := fnIn + } + } + + function _getManifest() internal view returns (PluginManifest memory) { + PluginManifest memory m; + + // Always add the execution function + m.executionFunctions = new bytes4[](1); + m.executionFunctions[0] = this.executionFunction.selector; + + // Conditionally add the other functions + + if (hasPreUOValidation) { + m.preUserOpValidationHooks = new ManifestAssociatedFunction[](1); + m.preUserOpValidationHooks[0] = ManifestAssociatedFunction({ + executionSelector: this.executionFunction.selector, + associatedFunction: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: uint8(FunctionId.PRE_USER_OP_VALIDATION_HOOK), + dependencyIndex: 0 // Unused + }) + }); + } + + if (hasUOValidation) { + m.userOpValidationFunctions = new ManifestAssociatedFunction[](1); + m.userOpValidationFunctions[0] = ManifestAssociatedFunction({ + executionSelector: this.executionFunction.selector, + associatedFunction: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: uint8(FunctionId.USER_OP_VALIDATION), + dependencyIndex: 0 // Unused + }) + }); + } + + if (hasPreRTValidation) { + m.preRuntimeValidationHooks = new ManifestAssociatedFunction[](1); + m.preRuntimeValidationHooks[0] = ManifestAssociatedFunction({ + executionSelector: this.executionFunction.selector, + associatedFunction: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: uint8(FunctionId.PRE_RUNTIME_VALIDATION_HOOK), + dependencyIndex: 0 // Unused + }) + }); + } + + if (hasRTValidation) { + m.runtimeValidationFunctions = new ManifestAssociatedFunction[](1); + m.runtimeValidationFunctions[0] = ManifestAssociatedFunction({ + executionSelector: this.executionFunction.selector, + associatedFunction: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: uint8(FunctionId.RUNTIME_VALIDATION), + dependencyIndex: 0 // Unused + }) + }); + } + + if (hasPreExec && hasPostExec) { + m.executionHooks = new ManifestExecutionHook[](1); + m.executionHooks[0] = ManifestExecutionHook({ + executionSelector: this.executionFunction.selector, + preExecHook: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: uint8(FunctionId.PRE_EXECUTION_HOOK), + dependencyIndex: 0 // Unused + }), + postExecHook: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: uint8(FunctionId.POST_EXECUTION_HOOK), + dependencyIndex: 0 // Unused + }) + }); + } else if (hasPreExec) { + m.executionHooks = new ManifestExecutionHook[](1); + m.executionHooks[0] = ManifestExecutionHook({ + executionSelector: this.executionFunction.selector, + preExecHook: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: uint8(FunctionId.PRE_EXECUTION_HOOK), + dependencyIndex: 0 // Unused + }), + postExecHook: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.NONE, + functionId: 0, // Unused + dependencyIndex: 0 // Unused + }) + }); + } else if (hasPostExec) { + m.executionHooks = new ManifestExecutionHook[](1); + m.executionHooks[0] = ManifestExecutionHook({ + executionSelector: this.executionFunction.selector, + preExecHook: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.NONE, + functionId: 0, // Unused + dependencyIndex: 0 // Unused + }), + postExecHook: ManifestFunction({ + functionType: ManifestAssociatedFunctionType.SELF, + functionId: uint8(FunctionId.POST_EXECUTION_HOOK), + dependencyIndex: 0 // Unused + }) + }); + } + + return m; + } + + // Empty implementations of install/uninstall + + function onInstall(bytes calldata) external override {} + + function onUninstall(bytes calldata) external override {} + + // Plugin functions + + function preUserOpValidationHook(uint8 functionId, UserOperation calldata, bytes32) + external + override + returns (uint256) + { + if (functionId == uint8(FunctionId.PRE_USER_OP_VALIDATION_HOOK)) { + _performCallbackIfNonempty(preUOValidationCallback); + return 0; + } + revert NotImplemented(); + } + + function userOpValidationFunction(uint8 functionId, UserOperation calldata, bytes32) + external + override + returns (uint256) + { + if (functionId == uint8(FunctionId.USER_OP_VALIDATION)) { + _performCallbackIfNonempty(UOValidationCallback); + return 0; + } + revert NotImplemented(); + } + + function preRuntimeValidationHook(uint8 functionId, address, uint256, bytes calldata) external override { + if (functionId == uint8(FunctionId.PRE_RUNTIME_VALIDATION_HOOK)) { + _performCallbackIfNonempty(preRTValidationCallback); + return; + } + revert NotImplemented(); + } + + function runtimeValidationFunction(uint8 functionId, address, uint256, bytes calldata) external override { + if (functionId == uint8(FunctionId.RUNTIME_VALIDATION)) { + _performCallbackIfNonempty(RTValidationCallback); + return; + } + revert NotImplemented(); + } + + function preExecutionHook(uint8 functionId, address, uint256, bytes calldata) + external + override + returns (bytes memory) + { + if (functionId == uint8(FunctionId.PRE_EXECUTION_HOOK)) { + _performCallbackIfNonempty(preExecCallback); + return ""; + } + revert NotImplemented(); + } + + function executionFunction() external { + _performCallbackIfNonempty(execCallback); + } + + function postExecutionHook(uint8 functionId, bytes calldata) external override { + if (functionId == uint8(FunctionId.POST_EXECUTION_HOOK)) { + _performCallbackIfNonempty(postExecCallback); + return; + } + revert NotImplemented(); + } + + function _performCallbackIfNonempty(bytes storage callback) internal { + if (callback.length > 0) { + (bool success,) = msg.sender.call(callback); + require(success, "Callback failed"); + } + } +} diff --git a/test/mocks/plugins/ComprehensivePlugin.sol b/test/mocks/plugins/ComprehensivePlugin.sol index 16029de7b..31e584191 100644 --- a/test/mocks/plugins/ComprehensivePlugin.sol +++ b/test/mocks/plugins/ComprehensivePlugin.sol @@ -117,6 +117,9 @@ contract ComprehensivePlugin is BaseTestPlugin { function pluginManifest() external pure override returns (PluginManifest memory) { PluginManifest memory manifest; + manifest.permittedExecutionSelectors = new bytes4[](1); + manifest.permittedExecutionSelectors[0] = this.foo.selector; + manifest.executionFunctions = new bytes4[](1); manifest.executionFunctions[0] = this.foo.selector; diff --git a/test/plugin/TokenReceiverPlugin.t.sol b/test/plugin/TokenReceiverPlugin.t.sol index 35b1f8b82..1ab6543d8 100644 --- a/test/plugin/TokenReceiverPlugin.t.sol +++ b/test/plugin/TokenReceiverPlugin.t.sol @@ -93,7 +93,8 @@ contract TokenReceiverPluginTest is Test, IERC1155Receiver, AccountStorageV1 { function test_failERC721Transfer() public { vm.expectRevert( abi.encodeWithSelector( - UpgradeableModularAccount.UnrecognizedFunction.selector, IERC721Receiver.onERC721Received.selector + UpgradeableModularAccount.RuntimeValidationFunctionMissing.selector, + IERC721Receiver.onERC721Received.selector ) ); t0.safeTransferFrom(address(this), address(acct), _TOKEN_ID); @@ -109,7 +110,8 @@ contract TokenReceiverPluginTest is Test, IERC1155Receiver, AccountStorageV1 { function test_failERC777Transfer() public { vm.expectRevert( abi.encodeWithSelector( - UpgradeableModularAccount.UnrecognizedFunction.selector, IERC777Recipient.tokensReceived.selector + UpgradeableModularAccount.RuntimeValidationFunctionMissing.selector, + IERC777Recipient.tokensReceived.selector ) ); t1.transfer(address(acct), _TOKEN_AMOUNT);