From b002932eba6953ec178259ecf1e080f75c4a4fc0 Mon Sep 17 00:00:00 2001 From: adam Date: Fri, 6 Sep 2024 15:56:38 -0400 Subject: [PATCH] feat: port account tests --- src/account/AccountFactory.sol | 4 +- src/account/ModularAccount.sol | 2 +- src/account/SemiModularAccount.sol | 2 +- test/account/AccountExecHooks.t.sol | 184 ++++++ test/account/AccountFactory.t.sol | 38 ++ test/account/AccountReturnData.t.sol | 122 ++++ test/account/DirectCallsFromModule.t.sol | 186 ++++++ test/account/GlobalValidationTest.t.sol | 76 +++ test/account/ModularAccount.t.sol | 44 +- test/account/ModularAccountView.t.sol | 157 +++++ test/account/MultiValidation.t.sol | 130 +++++ test/account/PerHookData.t.sol | 539 ++++++++++++++++++ test/account/PermittedCallPermissions.t.sol | 54 ++ test/account/ReplaceModule.t.sol | 218 +++++++ test/account/SelfCallAuthorization.t.sol | 338 +++++++++++ test/account/ValidationIntersection.t.sol | 337 +++++++++++ .../ComprehensiveModule.sol | 0 test/mocks/modules/DirectCallModule.sol | 57 ++ .../modules/MockAccessControlHookModule.sol | 94 +++ test/mocks/{module => modules}/MockModule.sol | 0 test/mocks/modules/PermittedCallMocks.sol | 45 ++ test/mocks/modules/ReturnDataModuleMocks.sol | 143 +++++ test/mocks/modules/ValidationModuleMocks.sol | 193 +++++++ test/utils/AccountTestBase.sol | 27 +- 24 files changed, 2972 insertions(+), 18 deletions(-) create mode 100644 test/account/AccountExecHooks.t.sol create mode 100644 test/account/AccountFactory.t.sol create mode 100644 test/account/AccountReturnData.t.sol create mode 100644 test/account/DirectCallsFromModule.t.sol create mode 100644 test/account/GlobalValidationTest.t.sol create mode 100644 test/account/ModularAccountView.t.sol create mode 100644 test/account/MultiValidation.t.sol create mode 100644 test/account/PerHookData.t.sol create mode 100644 test/account/PermittedCallPermissions.t.sol create mode 100644 test/account/ReplaceModule.t.sol create mode 100644 test/account/SelfCallAuthorization.t.sol create mode 100644 test/account/ValidationIntersection.t.sol rename test/mocks/{module => modules}/ComprehensiveModule.sol (100%) create mode 100644 test/mocks/modules/DirectCallModule.sol create mode 100644 test/mocks/modules/MockAccessControlHookModule.sol rename test/mocks/{module => modules}/MockModule.sol (100%) create mode 100644 test/mocks/modules/PermittedCallMocks.sol create mode 100644 test/mocks/modules/ReturnDataModuleMocks.sol create mode 100644 test/mocks/modules/ValidationModuleMocks.sol diff --git a/src/account/AccountFactory.sol b/src/account/AccountFactory.sol index 003ece94..1b8f39c8 100644 --- a/src/account/AccountFactory.sol +++ b/src/account/AccountFactory.sol @@ -114,7 +114,9 @@ contract AccountFactory is Ownable { } function _getAddressSemiModular(bytes memory immutables, bytes32 salt) internal view returns (address) { - return LibClone.predictDeterministicAddressERC1967(address(ACCOUNT_IMPL), immutables, salt, address(this)); + return LibClone.predictDeterministicAddressERC1967( + address(SEMI_MODULAR_ACCOUNT_IMPL), immutables, salt, address(this) + ); } function _getImmutableArgs(address owner) private pure returns (bytes memory) { diff --git a/src/account/ModularAccount.sol b/src/account/ModularAccount.sol index a1cfa815..33b246fd 100644 --- a/src/account/ModularAccount.sol +++ b/src/account/ModularAccount.sol @@ -298,7 +298,7 @@ contract ModularAccount is /// @inheritdoc IModularAccount function accountId() external pure virtual returns (string memory) { - return "erc6900.reference-modular-account.0.8.0"; + return "alchemy.modular-account.0.0.1"; } /// @inheritdoc UUPSUpgradeable diff --git a/src/account/SemiModularAccount.sol b/src/account/SemiModularAccount.sol index 1e510b60..be5ea5ab 100644 --- a/src/account/SemiModularAccount.sol +++ b/src/account/SemiModularAccount.sol @@ -98,7 +98,7 @@ contract SemiModularAccount is ModularAccount { /// @inheritdoc IModularAccount function accountId() external pure override returns (string memory) { - return "erc6900.reference-semi-modular-account.0.8.0"; + return "alchemy.semi-modular-account.0.0.1"; } function replaySafeHash(bytes32 hash) public view virtual returns (bytes32) { diff --git a/test/account/AccountExecHooks.t.sol b/test/account/AccountExecHooks.t.sol new file mode 100644 index 00000000..a99e6f2d --- /dev/null +++ b/test/account/AccountExecHooks.t.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import {IExecutionHookModule} from "@erc-6900/reference-implementation/interfaces/IExecutionHookModule.sol"; +import { + ExecutionManifest, + IModule, + ManifestExecutionFunction, + ManifestExecutionHook +} from "@erc-6900/reference-implementation/interfaces/IExecutionModule.sol"; + +import {MockModule} from "../mocks/modules/MockModule.sol"; +import {AccountTestBase} from "../utils/AccountTestBase.sol"; + +contract AccountExecHooksTest is AccountTestBase { + MockModule public mockModule1; + + bytes4 internal constant _EXEC_SELECTOR = bytes4(uint32(1)); + uint32 internal constant _PRE_HOOK_ENTITY_ID_1 = 1; + uint32 internal constant _POST_HOOK_ENTITY_ID_2 = 2; + uint32 internal constant _BOTH_HOOKS_ENTITY_ID_3 = 3; + + ExecutionManifest internal _m1; + + event ExecutionInstalled(address indexed module, ExecutionManifest manifest); + event ExecutionUninstalled(address indexed module, bool onUninstallSucceeded, ExecutionManifest manifest); + // emitted by MockModule + event ReceivedCall(bytes msgData, uint256 msgValue); + + function setUp() public { + _allowTestDirectCalls(); + + _m1.executionFunctions.push( + ManifestExecutionFunction({ + executionSelector: _EXEC_SELECTOR, + skipRuntimeValidation: true, + allowGlobalValidation: false + }) + ); + } + + function test_preExecHook_install() public { + _installExecution1WithHooks( + ManifestExecutionHook({ + executionSelector: _EXEC_SELECTOR, + entityId: _PRE_HOOK_ENTITY_ID_1, + isPreHook: true, + isPostHook: false + }) + ); + } + + /// @dev Module 1 hook pair: [1, null] + /// Expected execution: [1, null] + function test_preExecHook_run() public { + test_preExecHook_install(); + + vm.expectEmit(true, true, true, true); + emit ReceivedCall( + abi.encodeWithSelector( + IExecutionHookModule.preExecutionHook.selector, + _PRE_HOOK_ENTITY_ID_1, + address(this), // caller + uint256(0), // msg.value in call to account + abi.encodeWithSelector(_EXEC_SELECTOR) + ), + 0 // msg value in call to module + ); + + (bool success,) = address(account1).call(abi.encodeWithSelector(_EXEC_SELECTOR)); + assertTrue(success); + } + + function test_preExecHook_uninstall() public { + test_preExecHook_install(); + + _uninstallExecution(mockModule1); + } + + function test_execHookPair_install() public { + _installExecution1WithHooks( + ManifestExecutionHook({ + executionSelector: _EXEC_SELECTOR, + entityId: _BOTH_HOOKS_ENTITY_ID_3, + isPreHook: true, + isPostHook: true + }) + ); + } + + /// @dev Module 1 hook pair: [1, 2] + /// Expected execution: [1, 2] + function test_execHookPair_run() public { + test_execHookPair_install(); + + vm.expectEmit(true, true, true, true); + // pre hook call + emit ReceivedCall( + abi.encodeWithSelector( + IExecutionHookModule.preExecutionHook.selector, + _BOTH_HOOKS_ENTITY_ID_3, + address(this), // caller + uint256(0), // msg.value in call to account + abi.encodeWithSelector(_EXEC_SELECTOR) + ), + 0 // msg value in call to module + ); + vm.expectEmit(true, true, true, true); + // exec call + emit ReceivedCall(abi.encodePacked(_EXEC_SELECTOR), 0); + vm.expectEmit(true, true, true, true); + // post hook call + emit ReceivedCall( + abi.encodeCall(IExecutionHookModule.postExecutionHook, (_BOTH_HOOKS_ENTITY_ID_3, "")), + 0 // msg value in call to module + ); + + (bool success,) = address(account1).call(abi.encodeWithSelector(_EXEC_SELECTOR)); + assertTrue(success); + } + + function test_execHookPair_uninstall() public { + test_execHookPair_install(); + + _uninstallExecution(mockModule1); + } + + function test_postOnlyExecHook_install() public { + _installExecution1WithHooks( + ManifestExecutionHook({ + executionSelector: _EXEC_SELECTOR, + entityId: _POST_HOOK_ENTITY_ID_2, + isPreHook: false, + isPostHook: true + }) + ); + } + + /// @dev Module 1 hook pair: [null, 2] + /// Expected execution: [null, 2] + function test_postOnlyExecHook_run() public { + test_postOnlyExecHook_install(); + + vm.expectEmit(true, true, true, true); + emit ReceivedCall( + abi.encodeCall(IExecutionHookModule.postExecutionHook, (_POST_HOOK_ENTITY_ID_2, "")), + 0 // msg value in call to module + ); + + (bool success,) = address(account1).call(abi.encodeWithSelector(_EXEC_SELECTOR)); + assertTrue(success); + } + + function test_postOnlyExecHook_uninstall() public { + test_postOnlyExecHook_install(); + + _uninstallExecution(mockModule1); + } + + function _installExecution1WithHooks(ManifestExecutionHook memory execHooks) internal { + _m1.executionHooks.push(execHooks); + mockModule1 = new MockModule(_m1); + + vm.expectEmit(true, true, true, true); + emit ReceivedCall(abi.encodeCall(IModule.onInstall, (bytes("a"))), 0); + vm.expectEmit(true, true, true, true); + emit ExecutionInstalled(address(mockModule1), _m1); + + account1.installExecution({ + module: address(mockModule1), + manifest: mockModule1.executionManifest(), + moduleInstallData: bytes("a") + }); + } + + function _uninstallExecution(MockModule module) internal { + vm.expectEmit(true, true, true, true); + emit ReceivedCall(abi.encodeCall(IModule.onUninstall, (bytes("b"))), 0); + vm.expectEmit(true, true, true, true); + emit ExecutionUninstalled(address(module), true, module.executionManifest()); + + account1.uninstallExecution(address(module), module.executionManifest(), bytes("b")); + } +} diff --git a/test/account/AccountFactory.t.sol b/test/account/AccountFactory.t.sol new file mode 100644 index 00000000..fab7384a --- /dev/null +++ b/test/account/AccountFactory.t.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import {ModularAccount} from "../../src/account/ModularAccount.sol"; + +import {AccountTestBase} from "../utils/AccountTestBase.sol"; +import {TEST_DEFAULT_VALIDATION_ENTITY_ID} from "../utils/TestConstants.sol"; + +contract AccountFactoryTest is AccountTestBase { + function test_createAccount() public { + ModularAccount account = factory.createAccount(address(this), 100, TEST_DEFAULT_VALIDATION_ENTITY_ID); + + assertEq(address(account.entryPoint()), address(entryPoint)); + } + + function test_createAccountAndGetAddress() public { + ModularAccount account = factory.createAccount(address(this), 100, TEST_DEFAULT_VALIDATION_ENTITY_ID); + + assertEq( + address(account), address(factory.createAccount(address(this), 100, TEST_DEFAULT_VALIDATION_ENTITY_ID)) + ); + } + + function test_multipleDeploy() public { + ModularAccount account = factory.createAccount(address(this), 100, TEST_DEFAULT_VALIDATION_ENTITY_ID); + + uint256 startGas = gasleft(); + + ModularAccount account2 = factory.createAccount(address(this), 100, TEST_DEFAULT_VALIDATION_ENTITY_ID); + + // Assert that the 2nd deployment call cost less than 1 sstore + // Implies that no deployment was done on the second calls + assertLe(startGas - 22_000, gasleft()); + + // Assert the return addresses are the same + assertEq(address(account), address(account2)); + } +} diff --git a/test/account/AccountReturnData.t.sol b/test/account/AccountReturnData.t.sol new file mode 100644 index 00000000..b1bbcfa9 --- /dev/null +++ b/test/account/AccountReturnData.t.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import {Call} from "@erc-6900/reference-implementation/interfaces/IModularAccount.sol"; +import {IModularAccount} from "@erc-6900/reference-implementation/interfaces/IModularAccount.sol"; + +import {DIRECT_CALL_VALIDATION_ENTITYID} from "../../src/helpers/Constants.sol"; +import {ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; + +import { + RegularResultContract, + ResultConsumerModule, + ResultCreatorModule +} from "../mocks/modules/ReturnDataModuleMocks.sol"; +import {AccountTestBase} from "../utils/AccountTestBase.sol"; + +// Tests all the different ways that return data can be read from modules through an account +contract AccountReturnDataTest is AccountTestBase { + RegularResultContract public regularResultContract; + ResultCreatorModule public resultCreatorModule; + ResultConsumerModule public resultConsumerModule; + + function setUp() public { + _transferOwnershipToTest(); + + regularResultContract = new RegularResultContract(); + resultCreatorModule = new ResultCreatorModule(); + resultConsumerModule = new ResultConsumerModule(resultCreatorModule, regularResultContract); + + // Add the result creator module to the account + vm.startPrank(address(entryPoint)); + account1.installExecution({ + module: address(resultCreatorModule), + manifest: resultCreatorModule.executionManifest(), + moduleInstallData: "" + }); + // Add the result consumer module to the account + account1.installExecution({ + module: address(resultConsumerModule), + manifest: resultConsumerModule.executionManifest(), + moduleInstallData: "" + }); + // Allow the result consumer module to perform direct calls to the account + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = IModularAccount.execute.selector; + account1.installValidation( + ValidationConfigLib.pack( + address(resultConsumerModule), DIRECT_CALL_VALIDATION_ENTITYID, false, false, true + ), // todo: does this need UO validation permission? + selectors, + "", + new bytes[](0) + ); + vm.stopPrank(); + } + + // Tests the ability to read the result of module execution functions via the account's fallback + function test_returnData_fallback() public view { + bytes32 result = ResultCreatorModule(address(account1)).foo(); + + assertEq(result, keccak256("bar")); + } + + // Tests the ability to read the results of contracts called via IModularAccount.execute + function test_returnData_singular_execute() public { + bytes memory returnData = account1.executeWithAuthorization( + abi.encodeCall( + account1.execute, + (address(regularResultContract), 0, abi.encodeCall(RegularResultContract.foo, ())) + ), + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, "") + ); + + bytes32 result = abi.decode(abi.decode(returnData, (bytes)), (bytes32)); + + assertEq(result, keccak256("bar")); + } + + // Tests the ability to read the results of multiple contract calls via IModularAccount.executeBatch + function test_returnData_executeBatch() public { + Call[] memory calls = new Call[](2); + calls[0] = Call({ + target: address(regularResultContract), + value: 0, + data: abi.encodeCall(RegularResultContract.foo, ()) + }); + calls[1] = Call({ + target: address(regularResultContract), + value: 0, + data: abi.encodeCall(RegularResultContract.bar, ()) + }); + + bytes memory retData = account1.executeWithAuthorization( + abi.encodeCall(account1.executeBatch, (calls)), + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, "") + ); + + bytes[] memory returnDatas = abi.decode(retData, (bytes[])); + + bytes32 result1 = abi.decode(returnDatas[0], (bytes32)); + bytes32 result2 = abi.decode(returnDatas[1], (bytes32)); + + assertEq(result1, keccak256("bar")); + assertEq(result2, keccak256("foo")); + } + + // Tests the ability to read data via routing to fallback functions + function test_returnData_execFromModule_fallback() public view { + bool result = ResultConsumerModule(address(account1)).checkResultFallback(keccak256("bar")); + + assertTrue(result); + } + + // Tests the ability to read data via executeWithAuthorization + function test_returnData_authorized_exec() public { + bool result = ResultConsumerModule(address(account1)).checkResultExecuteWithAuthorization( + address(regularResultContract), keccak256("bar") + ); + + assertTrue(result); + } +} diff --git a/test/account/DirectCallsFromModule.t.sol b/test/account/DirectCallsFromModule.t.sol new file mode 100644 index 00000000..80b1985d --- /dev/null +++ b/test/account/DirectCallsFromModule.t.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import {Call, IModularAccount} from "@erc-6900/reference-implementation/interfaces/IModularAccount.sol"; + +import {ModularAccount} from "../../src/account/ModularAccount.sol"; +import {DIRECT_CALL_VALIDATION_ENTITYID} from "../../src/helpers/Constants.sol"; +import {HookConfigLib} from "../../src/helpers/HookConfigLib.sol"; +import {ModuleEntity, ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; +import {ValidationConfig, ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; + +import {DirectCallModule} from "../mocks/modules/DirectCallModule.sol"; +import {AccountTestBase} from "../utils/AccountTestBase.sol"; + +contract DirectCallsFromModuleTest is AccountTestBase { + using ValidationConfigLib for ValidationConfig; + + DirectCallModule internal _module; + ModuleEntity internal _moduleEntity; + + event ValidationUninstalled(address indexed module, uint32 indexed entityId, bool onUninstallSucceeded); + + modifier randomizedValidationType(bool selectorValidation) { + if (selectorValidation) { + _installValidationSelector(); + } else { + _installValidationGlobal(); + } + _; + } + + function setUp() public { + _module = new DirectCallModule(); + assertFalse(_module.preHookRan()); + assertFalse(_module.postHookRan()); + _moduleEntity = ModuleEntityLib.pack(address(_module), DIRECT_CALL_VALIDATION_ENTITYID); + } + + /* -------------------------------------------------------------------------- */ + /* Negatives */ + /* -------------------------------------------------------------------------- */ + + function test_Fail_DirectCallModuleNotInstalled() external { + vm.prank(address(_module)); + vm.expectRevert(_buildDirectCallDisallowedError(IModularAccount.execute.selector)); + account1.execute(address(0), 0, ""); + } + + function testFuzz_Fail_DirectCallModuleUninstalled(bool validationType) + external + randomizedValidationType(validationType) + { + _uninstallValidation(); + + vm.prank(address(_module)); + vm.expectRevert(_buildDirectCallDisallowedError(IModularAccount.execute.selector)); + account1.execute(address(0), 0, ""); + } + + function test_Fail_DirectCallModuleCallOtherSelector() external { + _installValidationSelector(); + + Call[] memory calls = new Call[](0); + + vm.prank(address(_module)); + vm.expectRevert(_buildDirectCallDisallowedError(IModularAccount.executeBatch.selector)); + account1.executeBatch(calls); + } + + /* -------------------------------------------------------------------------- */ + /* Positives */ + /* -------------------------------------------------------------------------- */ + + function testFuzz_Pass_DirectCallFromModulePrank(bool validationType) + external + randomizedValidationType(validationType) + { + vm.prank(address(_module)); + account1.execute(address(0), 0, ""); + + assertTrue(_module.preHookRan()); + assertTrue(_module.postHookRan()); + } + + function testFuzz_Pass_DirectCallFromModuleCallback(bool validationType) + external + randomizedValidationType(validationType) + { + bytes memory encodedCall = abi.encodeCall(DirectCallModule.directCall, ()); + + vm.prank(address(entryPoint)); + bytes memory result = account1.execute(address(_module), 0, encodedCall); + + assertTrue(_module.preHookRan()); + assertTrue(_module.postHookRan()); + + // the directCall() function in the _module calls back into `execute()` with an encoded call back into the + // _module's getData() function. + assertEq(abi.decode(result, (bytes)), abi.encode(_module.getData())); + } + + function testFuzz_Flow_DirectCallFromModuleSequence(bool validationType) + external + randomizedValidationType(validationType) + { + // Install => Succeesfully call => uninstall => fail to call + + vm.prank(address(_module)); + account1.execute(address(0), 0, ""); + + assertTrue(_module.preHookRan()); + assertTrue(_module.postHookRan()); + + _uninstallValidation(); + + vm.prank(address(_module)); + vm.expectRevert(_buildDirectCallDisallowedError(IModularAccount.execute.selector)); + account1.execute(address(0), 0, ""); + } + + function test_directCallsFromEOA() external { + address extraOwner = makeAddr("extraOwner"); + + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = IModularAccount.execute.selector; + + vm.prank(address(entryPoint)); + + account1.installValidation( + ValidationConfigLib.pack(extraOwner, DIRECT_CALL_VALIDATION_ENTITYID, false, false, false), + selectors, + "", + new bytes[](0) + ); + + vm.prank(extraOwner); + account1.execute(makeAddr("dead"), 0, ""); + } + + /* -------------------------------------------------------------------------- */ + /* Internals */ + /* -------------------------------------------------------------------------- */ + + function _installValidationSelector() internal { + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = IModularAccount.execute.selector; + + bytes[] memory hooks = new bytes[](1); + hooks[0] = abi.encodePacked( + HookConfigLib.packExecHook({_hookFunction: _moduleEntity, _hasPre: true, _hasPost: true}), + hex"00" // onInstall data + ); + + vm.prank(address(entryPoint)); + + ValidationConfig validationConfig = ValidationConfigLib.pack(_moduleEntity, false, false, false); + + account1.installValidation(validationConfig, selectors, "", hooks); + } + + function _installValidationGlobal() internal { + bytes[] memory hooks = new bytes[](1); + hooks[0] = abi.encodePacked( + HookConfigLib.packExecHook({_hookFunction: _moduleEntity, _hasPre: true, _hasPost: true}), + hex"00" // onInstall data + ); + + vm.prank(address(entryPoint)); + + ValidationConfig validationConfig = ValidationConfigLib.pack(_moduleEntity, true, false, false); + + account1.installValidation(validationConfig, new bytes4[](0), "", hooks); + } + + function _uninstallValidation() internal { + (address module, uint32 entityId) = ModuleEntityLib.unpack(_moduleEntity); + vm.prank(address(entryPoint)); + vm.expectEmit(true, true, true, true); + emit ValidationUninstalled(module, entityId, true); + account1.uninstallValidation(_moduleEntity, "", new bytes[](1)); + } + + function _buildDirectCallDisallowedError(bytes4 selector) internal pure returns (bytes memory) { + return abi.encodeWithSelector(ModularAccount.ValidationFunctionMissing.selector, selector); + } +} diff --git a/test/account/GlobalValidationTest.t.sol b/test/account/GlobalValidationTest.t.sol new file mode 100644 index 00000000..69642b48 --- /dev/null +++ b/test/account/GlobalValidationTest.t.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +import {ModularAccount} from "../../src/account/ModularAccount.sol"; +import {ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; + +import {AccountTestBase} from "../utils/AccountTestBase.sol"; + +contract GlobalValidationTest is AccountTestBase { + using MessageHashUtils for bytes32; + + address public ethRecipient; + + // A separate account and owner that isn't deployed yet, used to test initcode + address public owner2; + uint256 public owner2Key; + ModularAccount public account2; + + function setUp() public { + (owner2, owner2Key) = makeAddrAndKey("owner2"); + + // Compute counterfactual address + account2 = ModularAccount(payable(factory.getAddress(owner2, 0, TEST_DEFAULT_VALIDATION_ENTITY_ID))); + vm.deal(address(account2), 100 ether); + + _signerValidation = + ModuleEntityLib.pack(address(singleSignerValidationModule), TEST_DEFAULT_VALIDATION_ENTITY_ID); + + ethRecipient = makeAddr("ethRecipient"); + vm.deal(ethRecipient, 1 wei); + } + + function test_globalValidation_userOp_simple() public { + PackedUserOperation memory userOp = PackedUserOperation({ + sender: address(account2), + nonce: 0, + initCode: abi.encodePacked( + address(factory), abi.encodeCall(factory.createAccount, (owner2, 0, TEST_DEFAULT_VALIDATION_ENTITY_ID)) + ), + callData: abi.encodeCall(ModularAccount.execute, (ethRecipient, 1 wei, "")), + accountGasLimits: _encodeGas(VERIFICATION_GAS_LIMIT, CALL_GAS_LIMIT), + preVerificationGas: 0, + gasFees: _encodeGas(1, 1), + paymasterAndData: "", + signature: "" + }); + + // Generate signature + bytes32 userOpHash = entryPoint.getUserOpHash(userOp); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner2Key, userOpHash.toEthSignedMessageHash()); + userOp.signature = _encodeSignature(_signerValidation, GLOBAL_VALIDATION, abi.encodePacked(r, s, v)); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + entryPoint.handleOps(userOps, beneficiary); + + assertEq(ethRecipient.balance, 2 wei); + } + + function test_globalValidation_runtime_simple() public { + // Deploy the account first + factory.createAccount(owner2, 0, TEST_DEFAULT_VALIDATION_ENTITY_ID); + + vm.prank(owner2); + account2.executeWithAuthorization( + abi.encodeCall(ModularAccount.execute, (ethRecipient, 1 wei, "")), + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, "") + ); + + assertEq(ethRecipient.balance, 2 wei); + } +} diff --git a/test/account/ModularAccount.t.sol b/test/account/ModularAccount.t.sol index 8c30f5d5..315a379a 100644 --- a/test/account/ModularAccount.t.sol +++ b/test/account/ModularAccount.t.sol @@ -22,9 +22,8 @@ import {TokenReceiverModule} from "../../src/modules/TokenReceiverModule.sol"; import {SingleSignerValidationModule} from "../../src/modules/validation/SingleSignerValidationModule.sol"; import {Counter} from "../mocks/Counter.sol"; - -import {ComprehensiveModule} from "../mocks/module/ComprehensiveModule.sol"; -import {MockModule} from "../mocks/module/MockModule.sol"; +import {ComprehensiveModule} from "../mocks/modules/ComprehensiveModule.sol"; +import {MockModule} from "../mocks/modules/MockModule.sol"; import {AccountTestBase} from "../utils/AccountTestBase.sol"; import {TEST_DEFAULT_VALIDATION_ENTITY_ID} from "../utils/TestConstants.sol"; @@ -53,7 +52,11 @@ contract ModularAccountTest is AccountTestBase { (owner2, owner2Key) = makeAddrAndKey("owner2"); // Compute counterfactual address - account2 = ModularAccount(payable(factory.getAddress(owner2, 0, TEST_DEFAULT_VALIDATION_ENTITY_ID))); + if (vm.envOr("SMA_TEST", false)) { + account2 = ModularAccount(payable(factory.getAddressSemiModular(owner2, 0))); + } else { + account2 = ModularAccount(payable(factory.getAddress(owner2, 0, TEST_DEFAULT_VALIDATION_ENTITY_ID))); + } vm.deal(address(account2), 100 ether); ethRecipient = makeAddr("ethRecipient"); @@ -106,12 +109,16 @@ contract ModularAccountTest is AccountTestBase { ) ); + bytes memory initCode = vm.envOr("SMA_TEST", false) + ? abi.encodePacked(address(factory), abi.encodeCall(factory.createSemiModularAccount, (owner2, 0))) + : abi.encodePacked( + address(factory), abi.encodeCall(factory.createAccount, (owner2, 0, TEST_DEFAULT_VALIDATION_ENTITY_ID)) + ); + PackedUserOperation memory userOp = PackedUserOperation({ sender: address(account2), nonce: 0, - initCode: abi.encodePacked( - address(factory), abi.encodeCall(factory.createAccount, (owner2, 0, TEST_DEFAULT_VALIDATION_ENTITY_ID)) - ), + initCode: initCode, callData: callData, accountGasLimits: _encodeGas(VERIFICATION_GAS_LIMIT, CALL_GAS_LIMIT), preVerificationGas: 0, @@ -132,14 +139,18 @@ contract ModularAccountTest is AccountTestBase { } function test_standardExecuteEthSend_withInitcode() public { + bytes memory initCode = vm.envOr("SMA_TEST", false) + ? abi.encodePacked(address(factory), abi.encodeCall(factory.createSemiModularAccount, (owner2, 0))) + : abi.encodePacked( + address(factory), abi.encodeCall(factory.createAccount, (owner2, 0, TEST_DEFAULT_VALIDATION_ENTITY_ID)) + ); + address payable recipient = payable(makeAddr("recipient")); PackedUserOperation memory userOp = PackedUserOperation({ sender: address(account2), nonce: 0, - initCode: abi.encodePacked( - address(factory), abi.encodeCall(factory.createAccount, (owner2, 0, TEST_DEFAULT_VALIDATION_ENTITY_ID)) - ), + initCode: initCode, callData: abi.encodeCall(ModularAccount.execute, (recipient, 1 wei, "")), accountGasLimits: _encodeGas(VERIFICATION_GAS_LIMIT, CALL_GAS_LIMIT), preVerificationGas: 0, @@ -191,9 +202,7 @@ contract ModularAccountTest is AccountTestBase { string memory accountId = account1.accountId(); assertEq( accountId, - vm.envOr("SMA_TEST", false) - ? "erc6900.reference-semi-modular-account.0.8.0" - : "erc6900.reference-modular-account.0.8.0" + vm.envOr("SMA_TEST", false) ? "alchemy.semi-modular-account.0.0.1" : "alchemy.modular-account.0.0.1" ); } @@ -365,7 +374,14 @@ contract ModularAccountTest is AccountTestBase { bytes32 slot = account3.proxiableUUID(); // account has impl from factory - assertEq(address(accountImplementation), address(uint160(uint256(vm.load(address(account1), slot))))); + if (vm.envOr("SMA_TEST", false)) { + assertEq( + address(semiModularAccountImplementation), + address(uint160(uint256(vm.load(address(account1), slot)))) + ); + } else { + assertEq(address(accountImplementation), address(uint160(uint256(vm.load(address(account1), slot))))); + } account1.upgradeToAndCall(address(account3), bytes("")); // account has new impl assertEq(address(account3), address(uint160(uint256(vm.load(address(account1), slot))))); diff --git a/test/account/ModularAccountView.t.sol b/test/account/ModularAccountView.t.sol new file mode 100644 index 00000000..2a33f30a --- /dev/null +++ b/test/account/ModularAccountView.t.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; + +import {HookConfig, IModularAccount} from "@erc-6900/reference-implementation/interfaces/IModularAccount.sol"; +import { + ExecutionDataView, + ValidationDataView +} from "@erc-6900/reference-implementation/interfaces/IModularAccountView.sol"; + +import {HookConfigLib} from "../../src/helpers/HookConfigLib.sol"; +import {ModuleEntity, ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; + +import {ComprehensiveModule} from "../mocks/modules/ComprehensiveModule.sol"; +import {CustomValidationTestBase} from "../utils/CustomValidationTestBase.sol"; + +contract ModularAccountViewTest is CustomValidationTestBase { + ComprehensiveModule public comprehensiveModule; + + event ReceivedCall(bytes msgData, uint256 msgValue); + + ModuleEntity public comprehensiveModuleValidation; + + function setUp() public { + comprehensiveModule = new ComprehensiveModule(); + comprehensiveModuleValidation = + ModuleEntityLib.pack(address(comprehensiveModule), uint32(ComprehensiveModule.EntityId.VALIDATION)); + + _customValidationSetup(); + + vm.startPrank(address(entryPoint)); + account1.installExecution(address(comprehensiveModule), comprehensiveModule.executionManifest(), ""); + vm.stopPrank(); + } + + function test_moduleView_getExecutionData_native() public view { + bytes4[] memory selectorsToCheck = new bytes4[](5); + + selectorsToCheck[0] = IModularAccount.execute.selector; + + selectorsToCheck[1] = IModularAccount.executeBatch.selector; + + selectorsToCheck[2] = UUPSUpgradeable.upgradeToAndCall.selector; + + selectorsToCheck[3] = IModularAccount.installExecution.selector; + + selectorsToCheck[4] = IModularAccount.uninstallExecution.selector; + + for (uint256 i = 0; i < selectorsToCheck.length; i++) { + ExecutionDataView memory data = account1.getExecutionData(selectorsToCheck[i]); + assertEq(data.module, address(account1)); + assertTrue(data.allowGlobalValidation); + assertFalse(data.skipRuntimeValidation); + } + } + + function test_moduleView_getExecutionData_module() public view { + bytes4[] memory selectorsToCheck = new bytes4[](1); + address[] memory expectedModuleAddress = new address[](1); + + selectorsToCheck[0] = comprehensiveModule.foo.selector; + expectedModuleAddress[0] = address(comprehensiveModule); + + for (uint256 i = 0; i < selectorsToCheck.length; i++) { + ExecutionDataView memory data = account1.getExecutionData(selectorsToCheck[i]); + assertEq(data.module, expectedModuleAddress[i]); + assertFalse(data.allowGlobalValidation); + assertFalse(data.skipRuntimeValidation); + + HookConfig[3] memory expectedHooks = [ + HookConfigLib.packExecHook( + ModuleEntityLib.pack( + address(comprehensiveModule), uint32(ComprehensiveModule.EntityId.BOTH_EXECUTION_HOOKS) + ), + true, + true + ), + HookConfigLib.packExecHook( + ModuleEntityLib.pack( + address(comprehensiveModule), uint32(ComprehensiveModule.EntityId.PRE_EXECUTION_HOOK) + ), + true, + false + ), + HookConfigLib.packExecHook( + ModuleEntityLib.pack( + address(comprehensiveModule), uint32(ComprehensiveModule.EntityId.POST_EXECUTION_HOOK) + ), + false, + true + ) + ]; + + assertEq(data.executionHooks.length, 3); + for (uint256 j = 0; j < data.executionHooks.length; j++) { + assertEq(HookConfig.unwrap(data.executionHooks[j]), HookConfig.unwrap(expectedHooks[j])); + } + } + } + + function test_moduleView_getValidationData() public view { + ValidationDataView memory data = account1.getValidationData(comprehensiveModuleValidation); + bytes4[] memory selectors = data.selectors; + + assertTrue(data.isGlobal); + assertTrue(data.isSignatureValidation); + assertTrue(data.isUserOpValidation); + assertEq(data.preValidationHooks.length, 2); + assertEq( + ModuleEntity.unwrap(data.preValidationHooks[0]), + ModuleEntity.unwrap( + ModuleEntityLib.pack( + address(comprehensiveModule), uint32(ComprehensiveModule.EntityId.PRE_VALIDATION_HOOK_1) + ) + ) + ); + assertEq( + ModuleEntity.unwrap(data.preValidationHooks[1]), + ModuleEntity.unwrap( + ModuleEntityLib.pack( + address(comprehensiveModule), uint32(ComprehensiveModule.EntityId.PRE_VALIDATION_HOOK_2) + ) + ) + ); + + assertEq(data.executionHooks.length, 0); + assertEq(selectors.length, 1); + assertEq(selectors[0], comprehensiveModule.foo.selector); + } + + // Test config + + function _initialValidationConfig() + internal + virtual + override + returns (ModuleEntity, bool, bool, bool, bytes4[] memory, bytes memory, bytes[] memory) + { + bytes[] memory hooks = new bytes[](2); + hooks[0] = abi.encodePacked( + HookConfigLib.packValidationHook( + address(comprehensiveModule), uint32(ComprehensiveModule.EntityId.PRE_VALIDATION_HOOK_1) + ) + ); + hooks[1] = abi.encodePacked( + HookConfigLib.packValidationHook( + address(comprehensiveModule), uint32(ComprehensiveModule.EntityId.PRE_VALIDATION_HOOK_2) + ) + ); + + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = comprehensiveModule.foo.selector; + + return (comprehensiveModuleValidation, true, true, true, selectors, bytes(""), hooks); + } +} diff --git a/test/account/MultiValidation.t.sol b/test/account/MultiValidation.t.sol new file mode 100644 index 00000000..5e68fc61 --- /dev/null +++ b/test/account/MultiValidation.t.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntryPoint.sol"; +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +import {IModularAccount, ModuleEntity} from "@erc-6900/reference-implementation/interfaces/IModularAccount.sol"; + +import {ModularAccount} from "../../src/account/ModularAccount.sol"; +import {ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; +import {ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; +import {SingleSignerValidationModule} from "../../src/modules/validation/SingleSignerValidationModule.sol"; + +import {AccountTestBase} from "../utils/AccountTestBase.sol"; +import {TEST_DEFAULT_VALIDATION_ENTITY_ID} from "../utils/TestConstants.sol"; + +contract MultiValidationTest is AccountTestBase { + using ECDSA for bytes32; + using MessageHashUtils for bytes32; + + SingleSignerValidationModule public validator2; + + address public owner2; + uint256 public owner2Key; + + function setUp() public { + validator2 = new SingleSignerValidationModule(); + + (owner2, owner2Key) = makeAddrAndKey("owner2"); + } + + function test_overlappingValidationInstall() public { + vm.prank(address(entryPoint)); + account1.installValidation( + ValidationConfigLib.pack(address(validator2), TEST_DEFAULT_VALIDATION_ENTITY_ID, true, true, true), + new bytes4[](0), + abi.encode(TEST_DEFAULT_VALIDATION_ENTITY_ID, owner2), + new bytes[](0) + ); + + ModuleEntity[] memory validations = new ModuleEntity[](2); + validations[0] = _signerValidation; + validations[1] = ModuleEntityLib.pack(address(validator2), TEST_DEFAULT_VALIDATION_ENTITY_ID); + + bytes4[] memory selectors0 = account1.getValidationData(validations[0]).selectors; + bytes4[] memory selectors1 = account1.getValidationData(validations[1]).selectors; + assertEq(selectors0.length, selectors1.length); + for (uint256 i = 0; i < selectors0.length; i++) { + assertEq(selectors0[i], selectors1[i]); + } + } + + function test_runtimeValidation_specify() public { + test_overlappingValidationInstall(); + + // Assert that the runtime validation can be specified. + + vm.prank(owner1); + vm.expectRevert( + abi.encodeWithSelector( + ModularAccount.RuntimeValidationFunctionReverted.selector, + address(validator2), + TEST_DEFAULT_VALIDATION_ENTITY_ID, + abi.encodeWithSignature("NotAuthorized()") + ) + ); + account1.executeWithAuthorization( + abi.encodeCall(IModularAccount.execute, (address(0), 0, "")), + _encodeSignature( + ModuleEntityLib.pack(address(validator2), TEST_DEFAULT_VALIDATION_ENTITY_ID), GLOBAL_VALIDATION, "" + ) + ); + + vm.prank(owner2); + account1.executeWithAuthorization( + abi.encodeCall(IModularAccount.execute, (address(0), 0, "")), + _encodeSignature( + ModuleEntityLib.pack(address(validator2), TEST_DEFAULT_VALIDATION_ENTITY_ID), GLOBAL_VALIDATION, "" + ) + ); + } + + function test_userOpValidation_specify() public { + test_overlappingValidationInstall(); + + // Assert that the userOp validation can be specified. + + PackedUserOperation memory userOp = PackedUserOperation({ + sender: address(account1), + nonce: 0, + initCode: "", + callData: abi.encodeCall(ModularAccount.execute, (address(0), 0, "")), + accountGasLimits: _encodeGas(VERIFICATION_GAS_LIMIT, CALL_GAS_LIMIT), + preVerificationGas: 0, + gasFees: _encodeGas(1, 1), + paymasterAndData: "", + signature: "" + }); + + // Generate signature + bytes32 userOpHash = entryPoint.getUserOpHash(userOp); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner2Key, userOpHash.toEthSignedMessageHash()); + userOp.signature = _encodeSignature( + ModuleEntityLib.pack(address(validator2), TEST_DEFAULT_VALIDATION_ENTITY_ID), + GLOBAL_VALIDATION, + abi.encodePacked(r, s, v) + ); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + entryPoint.handleOps(userOps, beneficiary); + + // Sign with owner 1, expect fail + + userOp.nonce = 1; + (v, r, s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); + userOp.signature = _encodeSignature( + ModuleEntityLib.pack(address(validator2), TEST_DEFAULT_VALIDATION_ENTITY_ID), + GLOBAL_VALIDATION, + abi.encodePacked(r, s, v) + ); + + userOps[0] = userOp; + vm.expectRevert(abi.encodeWithSelector(IEntryPoint.FailedOp.selector, 0, "AA24 signature error")); + entryPoint.handleOps(userOps, beneficiary); + } +} diff --git a/test/account/PerHookData.t.sol b/test/account/PerHookData.t.sol new file mode 100644 index 00000000..c7c46355 --- /dev/null +++ b/test/account/PerHookData.t.sol @@ -0,0 +1,539 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntryPoint.sol"; +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +import {ModularAccount} from "../../src/account/ModularAccount.sol"; +import {HookConfigLib} from "../../src/helpers/HookConfigLib.sol"; +import {ModuleEntity, ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; +import {SparseCalldataSegmentLib} from "../../src/helpers/SparseCalldataSegmentLib.sol"; +import {ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; + +import {Counter} from "../mocks/Counter.sol"; +import {MockAccessControlHookModule} from "../mocks/modules/MockAccessControlHookModule.sol"; +import {CustomValidationTestBase} from "../utils/CustomValidationTestBase.sol"; + +contract PerHookDataTest is CustomValidationTestBase { + using MessageHashUtils for bytes32; + + MockAccessControlHookModule internal _accessControlHookModule; + + Counter internal _counter; + + uint32 internal constant _VALIDATION_ENTITY_ID = 0; + uint32 internal constant _PRE_HOOK_ENTITY_ID_1 = 0; + uint32 internal constant _PRE_HOOK_ENTITY_ID_2 = 1; + + function setUp() public { + _counter = new Counter(); + + _accessControlHookModule = new MockAccessControlHookModule(); + + _customValidationSetup(); + } + + function test_passAccessControl_userOp() public { + assertEq(_counter.number(), 0); + + (PackedUserOperation memory userOp, bytes32 userOpHash) = _getCounterUserOP(); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); + + PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](1); + preValidationHookData[0] = PreValidationHookData({index: 0, validationData: abi.encodePacked(_counter)}); + + userOp.signature = _encodeSignature( + _signerValidation, GLOBAL_VALIDATION, preValidationHookData, abi.encodePacked(r, s, v) + ); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + entryPoint.handleOps(userOps, beneficiary); + + assertEq(_counter.number(), 1); + } + + function test_failAccessControl_badSigData_userOp() public { + (PackedUserOperation memory userOp, bytes32 userOpHash) = _getCounterUserOP(); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); + + PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](1); + preValidationHookData[0] = PreValidationHookData({ + index: 0, + validationData: abi.encodePacked(address(0x1234123412341234123412341234123412341234)) + }); + + userOp.signature = _encodeSignature( + _signerValidation, GLOBAL_VALIDATION, preValidationHookData, abi.encodePacked(r, s, v) + ); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + vm.expectRevert( + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSignature("Error(string)", "Proof doesn't match target") + ) + ); + entryPoint.handleOps(userOps, beneficiary); + } + + function test_failAccessControl_noSigData_userOp() public { + (PackedUserOperation memory userOp, bytes32 userOpHash) = _getCounterUserOP(); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); + + userOp.signature = _encodeSignature(_signerValidation, GLOBAL_VALIDATION, abi.encodePacked(r, s, v)); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + vm.expectRevert( + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSignature("Error(string)", "Proof doesn't match target") + ) + ); + entryPoint.handleOps(userOps, beneficiary); + } + + function test_failAccessControl_badIndexProvided_userOp() public { + (PackedUserOperation memory userOp, bytes32 userOpHash) = _getCounterUserOP(); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); + + PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](2); + preValidationHookData[0] = PreValidationHookData({index: 0, validationData: abi.encodePacked(_counter)}); + preValidationHookData[1] = PreValidationHookData({index: 1, validationData: abi.encodePacked(_counter)}); + + userOp.signature = _encodeSignature( + _signerValidation, GLOBAL_VALIDATION, preValidationHookData, abi.encodePacked(r, s, v) + ); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + vm.expectRevert( + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSelector(SparseCalldataSegmentLib.ValidationSignatureSegmentMissing.selector) + ) + ); + entryPoint.handleOps(userOps, beneficiary); + } + + function test_passAccessControl_twoHooks_userOp() public { + _installSecondPreHook(); + + assertEq(_counter.number(), 0); + + (PackedUserOperation memory userOp, bytes32 userOpHash) = _getCounterUserOP(); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); + + PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](2); + preValidationHookData[0] = PreValidationHookData({index: 0, validationData: abi.encodePacked(_counter)}); + preValidationHookData[1] = PreValidationHookData({index: 1, validationData: abi.encodePacked(_counter)}); + + userOp.signature = _encodeSignature( + _signerValidation, GLOBAL_VALIDATION, preValidationHookData, abi.encodePacked(r, s, v) + ); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + entryPoint.handleOps(userOps, beneficiary); + + assertEq(_counter.number(), 1); + } + + function test_failAccessControl_indexOutOfOrder_userOp() public { + _installSecondPreHook(); + + (PackedUserOperation memory userOp, bytes32 userOpHash) = _getCounterUserOP(); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); + + PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](3); + preValidationHookData[0] = PreValidationHookData({index: 0, validationData: abi.encodePacked(_counter)}); + preValidationHookData[1] = PreValidationHookData({index: 0, validationData: abi.encodePacked(_counter)}); + + userOp.signature = _encodeSignature( + _signerValidation, GLOBAL_VALIDATION, preValidationHookData, abi.encodePacked(r, s, v) + ); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + vm.expectRevert( + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSelector(SparseCalldataSegmentLib.SegmentOutOfOrder.selector) + ) + ); + entryPoint.handleOps(userOps, beneficiary); + } + + function test_failAccessControl_badTarget_userOp() public { + PackedUserOperation memory userOp = PackedUserOperation({ + sender: address(account1), + nonce: 0, + initCode: "", + callData: abi.encodeCall(ModularAccount.execute, (beneficiary, 1 wei, "")), + accountGasLimits: _encodeGas(VERIFICATION_GAS_LIMIT, CALL_GAS_LIMIT), + preVerificationGas: 0, + gasFees: _encodeGas(1, 1), + paymasterAndData: "", + signature: "" + }); + + bytes32 userOpHash = entryPoint.getUserOpHash(userOp); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); + + PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](1); + preValidationHookData[0] = PreValidationHookData({index: 0, validationData: abi.encodePacked(beneficiary)}); + + userOp.signature = _encodeSignature( + _signerValidation, GLOBAL_VALIDATION, preValidationHookData, abi.encodePacked(r, s, v) + ); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + vm.expectRevert( + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSignature("Error(string)", "Target not allowed") + ) + ); + entryPoint.handleOps(userOps, beneficiary); + } + + function test_failPerHookData_nonCanonicalEncoding_userOp() public { + (PackedUserOperation memory userOp, bytes32 userOpHash) = _getCounterUserOP(); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); + + PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](1); + preValidationHookData[0] = PreValidationHookData({index: 0, validationData: ""}); + + userOp.signature = _encodeSignature( + _signerValidation, GLOBAL_VALIDATION, preValidationHookData, abi.encodePacked(r, s, v) + ); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + vm.expectRevert( + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSelector(SparseCalldataSegmentLib.NonCanonicalEncoding.selector) + ) + ); + entryPoint.handleOps(userOps, beneficiary); + } + + function test_failPerHookData_excessData_userOp() public { + (PackedUserOperation memory userOp, bytes32 userOpHash) = _getCounterUserOP(); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); + + PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](1); + preValidationHookData[0] = PreValidationHookData({index: 0, validationData: abi.encodePacked(_counter)}); + + userOp.signature = abi.encodePacked( + _encodeSignature( + _signerValidation, GLOBAL_VALIDATION, preValidationHookData, abi.encodePacked(r, s, v) + ), + "extra data" + ); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + vm.expectRevert( + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSelector(SparseCalldataSegmentLib.NonCanonicalEncoding.selector) + ) + ); + entryPoint.handleOps(userOps, beneficiary); + } + + function test_passAccessControl_runtime() public { + assertEq(_counter.number(), 0); + + PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](1); + preValidationHookData[0] = PreValidationHookData({index: 0, validationData: abi.encodePacked(_counter)}); + + vm.prank(owner1); + account1.executeWithAuthorization( + abi.encodeCall( + ModularAccount.execute, (address(_counter), 0 wei, abi.encodeCall(Counter.increment, ())) + ), + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, preValidationHookData, "") + ); + + assertEq(_counter.number(), 1); + } + + function test_failAccessControl_badSigData_runtime() public { + PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](1); + preValidationHookData[0] = PreValidationHookData({ + index: 0, + validationData: abi.encodePacked(address(0x1234123412341234123412341234123412341234)) + }); + + vm.prank(owner1); + vm.expectRevert( + abi.encodeWithSelector( + ModularAccount.PreRuntimeValidationHookFailed.selector, + _accessControlHookModule, + _PRE_HOOK_ENTITY_ID_1, + abi.encodeWithSignature("Error(string)", "Proof doesn't match target") + ) + ); + account1.executeWithAuthorization( + abi.encodeCall( + ModularAccount.execute, (address(_counter), 0 wei, abi.encodeCall(Counter.increment, ())) + ), + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, preValidationHookData, "") + ); + } + + function test_failAccessControl_noSigData_runtime() public { + vm.prank(owner1); + vm.expectRevert( + abi.encodeWithSelector( + ModularAccount.PreRuntimeValidationHookFailed.selector, + _accessControlHookModule, + _PRE_HOOK_ENTITY_ID_1, + abi.encodeWithSignature("Error(string)", "Proof doesn't match target") + ) + ); + account1.executeWithAuthorization( + abi.encodeCall( + ModularAccount.execute, (address(_counter), 0 wei, abi.encodeCall(Counter.increment, ())) + ), + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, "") + ); + } + + function test_failAccessControl_badIndexProvided_runtime() public { + PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](2); + preValidationHookData[0] = PreValidationHookData({index: 0, validationData: abi.encodePacked(_counter)}); + preValidationHookData[1] = PreValidationHookData({index: 1, validationData: abi.encodePacked(_counter)}); + + vm.prank(owner1); + vm.expectRevert( + abi.encodeWithSelector(SparseCalldataSegmentLib.ValidationSignatureSegmentMissing.selector) + ); + account1.executeWithAuthorization( + abi.encodeCall( + ModularAccount.execute, (address(_counter), 0 wei, abi.encodeCall(Counter.increment, ())) + ), + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, preValidationHookData, "") + ); + } + + function test_passAccessControl_twoHooks_runtime() public { + _installSecondPreHook(); + + assertEq(_counter.number(), 0); + + PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](2); + preValidationHookData[0] = PreValidationHookData({index: 0, validationData: abi.encodePacked(_counter)}); + preValidationHookData[1] = PreValidationHookData({index: 1, validationData: abi.encodePacked(_counter)}); + + vm.prank(owner1); + account1.executeWithAuthorization( + abi.encodeCall( + ModularAccount.execute, (address(_counter), 0 wei, abi.encodeCall(Counter.increment, ())) + ), + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, preValidationHookData, "") + ); + + assertEq(_counter.number(), 1); + } + + function test_failAccessControl_indexOutOfOrder_runtime() public { + _installSecondPreHook(); + + PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](3); + preValidationHookData[0] = PreValidationHookData({index: 0, validationData: abi.encodePacked(_counter)}); + preValidationHookData[1] = PreValidationHookData({index: 0, validationData: abi.encodePacked(_counter)}); + + vm.prank(owner1); + vm.expectRevert(abi.encodeWithSelector(SparseCalldataSegmentLib.SegmentOutOfOrder.selector)); + account1.executeWithAuthorization( + abi.encodeCall( + ModularAccount.execute, (address(_counter), 0 wei, abi.encodeCall(Counter.increment, ())) + ), + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, preValidationHookData, "") + ); + } + + function test_failAccessControl_badTarget_runtime() public { + PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](1); + preValidationHookData[0] = PreValidationHookData({index: 0, validationData: abi.encodePacked(beneficiary)}); + + vm.prank(owner1); + vm.expectRevert( + abi.encodeWithSelector( + ModularAccount.PreRuntimeValidationHookFailed.selector, + _accessControlHookModule, + _PRE_HOOK_ENTITY_ID_1, + abi.encodeWithSignature("Error(string)", "Target not allowed") + ) + ); + account1.executeWithAuthorization( + abi.encodeCall(ModularAccount.execute, (beneficiary, 1 wei, "")), + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, preValidationHookData, "") + ); + } + + function test_failPerHookData_nonCanonicalEncoding_runtime() public { + PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](1); + preValidationHookData[0] = PreValidationHookData({index: 0, validationData: ""}); + + vm.prank(owner1); + vm.expectRevert(abi.encodeWithSelector(SparseCalldataSegmentLib.NonCanonicalEncoding.selector)); + account1.executeWithAuthorization( + abi.encodeCall( + ModularAccount.execute, (address(_counter), 0 wei, abi.encodeCall(Counter.increment, ())) + ), + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, preValidationHookData, "") + ); + } + + function test_failPerHookData_excessData_runtime() public { + PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](1); + preValidationHookData[0] = PreValidationHookData({index: 0, validationData: abi.encodePacked(_counter)}); + + vm.prank(owner1); + vm.expectRevert(abi.encodeWithSelector(SparseCalldataSegmentLib.NonCanonicalEncoding.selector)); + account1.executeWithAuthorization( + abi.encodeCall( + ModularAccount.execute, (address(_counter), 0 wei, abi.encodeCall(Counter.increment, ())) + ), + abi.encodePacked( + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, preValidationHookData, ""), "extra data" + ) + ); + } + + function test_pass1271AccessControl() public view { + string memory message = "Hello, world!"; + + bytes32 messageHash = keccak256(abi.encodePacked(message)); + + bytes32 replaySafeHash = singleSignerValidationModule.replaySafeHash(address(account1), messageHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, replaySafeHash); + + PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](1); + preValidationHookData[0] = PreValidationHookData({index: 0, validationData: abi.encodePacked(message)}); + + bytes4 result = account1.isValidSignature( + messageHash, _encode1271Signature(_signerValidation, preValidationHookData, abi.encodePacked(r, s, v)) + ); + + assertEq(result, bytes4(0x1626ba7e)); + } + + function test_fail1271AccessControl_badSigData() public { + string memory message = "Hello, world!"; + + bytes32 messageHash = keccak256(abi.encodePacked(message)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, messageHash); + + PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](1); + preValidationHookData[0] = PreValidationHookData({ + index: 0, + validationData: abi.encodePacked(address(0x1234123412341234123412341234123412341234)) + }); + + vm.expectRevert("Preimage not provided"); + account1.isValidSignature( + messageHash, _encode1271Signature(_signerValidation, preValidationHookData, abi.encodePacked(r, s, v)) + ); + } + + function test_fail1271AccessControl_noSigData() public { + string memory message = "Hello, world!"; + + bytes32 messageHash = keccak256(abi.encodePacked(message)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, messageHash); + + vm.expectRevert("Preimage not provided"); + account1.isValidSignature(messageHash, _encode1271Signature(_signerValidation, abi.encodePacked(r, s, v))); + } + + function _installSecondPreHook() internal { + // depends on the ability of `installValidation` to append hooks + bytes[] memory hooks = new bytes[](1); + hooks[0] = abi.encodePacked( + HookConfigLib.packValidationHook(address(_accessControlHookModule), _PRE_HOOK_ENTITY_ID_2), + abi.encode(_PRE_HOOK_ENTITY_ID_2, _counter) + ); + vm.prank(address(entryPoint)); + account1.installValidation( + ValidationConfigLib.pack(_signerValidation, true, false, true), new bytes4[](0), "", hooks + ); + } + + function _getCounterUserOP() internal view returns (PackedUserOperation memory, bytes32) { + PackedUserOperation memory userOp = PackedUserOperation({ + sender: address(account1), + nonce: 0, + initCode: "", + callData: abi.encodeCall( + ModularAccount.execute, (address(_counter), 0 wei, abi.encodeCall(Counter.increment, ())) + ), + accountGasLimits: _encodeGas(VERIFICATION_GAS_LIMIT, CALL_GAS_LIMIT), + preVerificationGas: 0, + gasFees: _encodeGas(1, 1), + paymasterAndData: "", + signature: "" + }); + + bytes32 userOpHash = entryPoint.getUserOpHash(userOp); + + return (userOp, userOpHash); + } + + // Test config + + function _initialValidationConfig() + internal + virtual + override + returns (ModuleEntity, bool, bool, bool, bytes4[] memory, bytes memory, bytes[] memory) + { + bytes[] memory hooks = new bytes[](1); + hooks[0] = abi.encodePacked( + HookConfigLib.packValidationHook(address(_accessControlHookModule), _PRE_HOOK_ENTITY_ID_1), + abi.encode(_PRE_HOOK_ENTITY_ID_1, _counter) + ); + // patched to also work during SMA tests by differentiating the validation + _signerValidation = ModuleEntityLib.pack(address(singleSignerValidationModule), _VALIDATION_ENTITY_ID); + return ( + _signerValidation, true, true, true, new bytes4[](0), abi.encode(_VALIDATION_ENTITY_ID, owner1), hooks + ); + } +} diff --git a/test/account/PermittedCallPermissions.t.sol b/test/account/PermittedCallPermissions.t.sol new file mode 100644 index 00000000..761822c0 --- /dev/null +++ b/test/account/PermittedCallPermissions.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import {ModularAccount} from "../../src/account/ModularAccount.sol"; + +import {PermittedCallerModule} from "../mocks/modules/PermittedCallMocks.sol"; +import {ResultCreatorModule} from "../mocks/modules/ReturnDataModuleMocks.sol"; +import {AccountTestBase} from "../utils/AccountTestBase.sol"; + +contract PermittedCallPermissionsTest is AccountTestBase { + ResultCreatorModule public resultCreatorModule; + + PermittedCallerModule public permittedCallerModule; + + function setUp() public { + _transferOwnershipToTest(); + resultCreatorModule = new ResultCreatorModule(); + + // Initialize the permitted caller modules, which will attempt to use the permissions system to authorize + // calls. + permittedCallerModule = new PermittedCallerModule(); + + // Add the result creator module to the account + vm.startPrank(address(entryPoint)); + account1.installExecution({ + module: address(resultCreatorModule), + manifest: resultCreatorModule.executionManifest(), + moduleInstallData: "" + }); + // Add the permitted caller module to the account + account1.installExecution({ + module: address(permittedCallerModule), + manifest: permittedCallerModule.executionManifest(), + moduleInstallData: "" + }); + vm.stopPrank(); + } + + function test_permittedCall_Allowed() public view { + bytes memory result = PermittedCallerModule(address(account1)).usePermittedCallAllowed(); + bytes32 actual = abi.decode(result, (bytes32)); + + assertEq(actual, keccak256("bar")); + } + + function test_permittedCall_NotAllowed() public { + vm.expectRevert( + abi.encodeWithSelector( + ModularAccount.ValidationFunctionMissing.selector, ResultCreatorModule.bar.selector + ) + ); + PermittedCallerModule(address(account1)).usePermittedCallNotAllowed(); + } +} diff --git a/test/account/ReplaceModule.t.sol b/test/account/ReplaceModule.t.sol new file mode 100644 index 00000000..57f24e8e --- /dev/null +++ b/test/account/ReplaceModule.t.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import {IExecutionHookModule} from "@erc-6900/reference-implementation/interfaces/IExecutionHookModule.sol"; +import { + ExecutionManifest, + ManifestExecutionFunction, + ManifestExecutionHook +} from "@erc-6900/reference-implementation/interfaces/IExecutionModule.sol"; +import { + Call, IModularAccount, ModuleEntity +} from "@erc-6900/reference-implementation/interfaces/IModularAccount.sol"; +import {IValidationHookModule} from "@erc-6900/reference-implementation/interfaces/IValidationHookModule.sol"; + +import {ModularAccount} from "../../src/account/ModularAccount.sol"; +import {HookConfigLib} from "../../src/helpers/HookConfigLib.sol"; +import {ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; +import {ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; +import {SingleSignerValidationModule} from "../../src/modules/validation/SingleSignerValidationModule.sol"; + +import {MockModule} from "../mocks/modules/MockModule.sol"; +import {AccountTestBase} from "../utils/AccountTestBase.sol"; + +interface TestModule { + function testFunction() external; +} + +contract UpgradeModuleTest is AccountTestBase { + address public target = address(1000); + uint256 public sendAmount = 1 ether; + uint32 public entityId = 10; + + // From MockModule + event ReceivedCall(bytes msgData, uint256 msgValue); + + function test_upgradeModuleExecutionFunction() public { + ExecutionManifest memory m; + ManifestExecutionFunction[] memory executionFunctions = new ManifestExecutionFunction[](1); + executionFunctions[0] = ManifestExecutionFunction({ + executionSelector: TestModule.testFunction.selector, + skipRuntimeValidation: true, + allowGlobalValidation: true + }); + m.executionFunctions = executionFunctions; + ManifestExecutionHook[] memory executionHooks = new ManifestExecutionHook[](1); + executionHooks[0] = ManifestExecutionHook({ + executionSelector: TestModule.testFunction.selector, + entityId: entityId, + isPreHook: true, + isPostHook: true + }); + m.executionHooks = executionHooks; + + MockModule moduleV1 = new MockModule(m); + MockModule moduleV2 = new MockModule(m); + vm.startPrank(address(entryPoint)); + account1.installExecution(address(moduleV1), moduleV1.executionManifest(), ""); + + // test installed + vm.expectEmit(true, true, true, true); + bytes memory callData = abi.encodePacked(TestModule.testFunction.selector); + emit ReceivedCall( + abi.encodeCall(IExecutionHookModule.preExecutionHook, (entityId, address(entryPoint), 0, callData)), 0 + ); + emit ReceivedCall(callData, 0); + TestModule(address(account1)).testFunction(); + + // upgrade module by batching uninstall + install calls + vm.startPrank(owner1); + Call[] memory calls = new Call[](2); + calls[0] = Call({ + target: address(account1), + value: 0, + data: abi.encodeCall( + IModularAccount.uninstallExecution, (address(moduleV1), moduleV1.executionManifest(), "") + ) + }); + calls[1] = Call({ + target: address(account1), + value: 0, + data: abi.encodeCall( + IModularAccount.installExecution, (address(moduleV2), moduleV2.executionManifest(), "") + ) + }); + account1.executeWithAuthorization( + abi.encodeCall(account1.executeBatch, (calls)), + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, "") + ); + + // test installed, test if old module still installed + assertEq(account1.getExecutionData((TestModule.testFunction.selector)).module, address(moduleV2)); + vm.expectEmit(true, true, true, true); + emit ReceivedCall( + abi.encodeCall(IExecutionHookModule.preExecutionHook, (entityId, address(owner1), 0, callData)), 0 + ); + emit ReceivedCall(abi.encodePacked(TestModule.testFunction.selector), 0); + TestModule(address(account1)).testFunction(); + } + + function test_upgradeModuleValidationFunction() public { + // Setup new validaiton with pre validation and execution hooks associated with a validator + SingleSignerValidationModule validation1 = new SingleSignerValidationModule(); + SingleSignerValidationModule validation2 = new SingleSignerValidationModule(); + uint32 validationEntityId1 = 10; + uint32 validationEntityId2 = 11; + + MockModule mockPreValAndPermissionsModule = new MockModule( + ExecutionManifest({ + executionFunctions: new ManifestExecutionFunction[](0), + executionHooks: new ManifestExecutionHook[](0), + interfaceIds: new bytes4[](0) + }) + ); + + ModuleEntity currModuleEntity = ModuleEntityLib.pack(address(validation1), validationEntityId1); + ModuleEntity newModuleEntity = ModuleEntityLib.pack(address(validation2), validationEntityId2); + + bytes[] memory hooksForVal1 = new bytes[](2); + hooksForVal1[0] = abi.encodePacked( + HookConfigLib.packValidationHook(address(mockPreValAndPermissionsModule), validationEntityId1) + ); + hooksForVal1[1] = abi.encodePacked( + HookConfigLib.packExecHook(address(mockPreValAndPermissionsModule), validationEntityId1, true, true) + ); + + vm.prank(address(entryPoint)); + account1.installValidation( + ValidationConfigLib.pack(currModuleEntity, true, false, true), + new bytes4[](0), + abi.encode(validationEntityId1, owner1), + hooksForVal1 + ); + // Test that setup worked. Pre val + pre exec hooks should run + vm.startPrank(owner1); + bytes memory callData = abi.encodeCall(IModularAccount.execute, (address(target), sendAmount, "")); + vm.expectEmit(true, true, true, true); + emit ReceivedCall( + abi.encodeCall( + IValidationHookModule.preRuntimeValidationHook, + (validationEntityId1, address(owner1), 0, callData, "") + ), + 0 + ); + emit ReceivedCall( + abi.encodeCall( + IExecutionHookModule.preExecutionHook, (validationEntityId1, address(owner1), 0, callData) + ), + 0 + ); + account1.executeWithAuthorization(callData, _encodeSignature(currModuleEntity, GLOBAL_VALIDATION, "")); + assertEq(target.balance, sendAmount); + + // upgrade module by batching uninstall + install calls + bytes[] memory hooksForVal2 = new bytes[](2); + hooksForVal2[0] = abi.encodePacked( + HookConfigLib.packValidationHook(address(mockPreValAndPermissionsModule), validationEntityId2) + ); + hooksForVal2[1] = abi.encodePacked( + HookConfigLib.packExecHook(address(mockPreValAndPermissionsModule), validationEntityId2, true, true) + ); + + bytes[] memory emptyBytesArr = new bytes[](0); + Call[] memory calls = new Call[](2); + calls[0] = Call({ + target: address(account1), + value: 0, + data: abi.encodeCall( + IModularAccount.uninstallValidation, (currModuleEntity, abi.encode(validationEntityId1), emptyBytesArr) + ) + }); + calls[1] = Call({ + target: address(account1), + value: 0, + data: abi.encodeCall( + IModularAccount.installValidation, + ( + ValidationConfigLib.pack(newModuleEntity, true, false, true), + new bytes4[](0), + abi.encode(validationEntityId2, owner1), + hooksForVal2 + ) + ) + }); + account1.executeWithAuthorization( + abi.encodeCall(account1.executeBatch, (calls)), + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, "") + ); + + // Test if old validation still works, expect fail + vm.expectRevert( + abi.encodePacked( + ModularAccount.ValidationFunctionMissing.selector, abi.encode(IModularAccount.execute.selector) + ) + ); + account1.executeWithAuthorization( + abi.encodeCall(IModularAccount.execute, (target, sendAmount, "")), + _encodeSignature(currModuleEntity, GLOBAL_VALIDATION, "") + ); + + // Test if new validation works + vm.expectEmit(true, true, true, true); + emit ReceivedCall( + abi.encodeCall( + IValidationHookModule.preRuntimeValidationHook, + (validationEntityId2, address(owner1), 0, callData, "") + ), + 0 + ); + emit ReceivedCall( + abi.encodeCall( + IExecutionHookModule.preExecutionHook, (validationEntityId2, address(entryPoint), 0, callData) + ), + 0 + ); + account1.executeWithAuthorization(callData, _encodeSignature(newModuleEntity, GLOBAL_VALIDATION, "")); + assertEq(target.balance, 2 * sendAmount); + } +} diff --git a/test/account/SelfCallAuthorization.t.sol b/test/account/SelfCallAuthorization.t.sol new file mode 100644 index 00000000..27c244e2 --- /dev/null +++ b/test/account/SelfCallAuthorization.t.sol @@ -0,0 +1,338 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import {IAccountExecute} from "@eth-infinitism/account-abstraction/interfaces/IAccountExecute.sol"; +import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntryPoint.sol"; +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; + +import {Call, IModularAccount} from "@erc-6900/reference-implementation/interfaces/IModularAccount.sol"; + +import {ModularAccount} from "../../src/account/ModularAccount.sol"; +import {ModuleEntity, ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; +import {ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; + +import {ComprehensiveModule} from "../mocks/modules/ComprehensiveModule.sol"; +import {AccountTestBase} from "../utils/AccountTestBase.sol"; + +contract SelfCallAuthorizationTest is AccountTestBase { + ComprehensiveModule public comprehensiveModule; + + ModuleEntity public comprehensiveModuleValidation; + + function setUp() public { + // install the comprehensive module to get new exec functions with different validations configured. + + comprehensiveModule = new ComprehensiveModule(); + + comprehensiveModuleValidation = + ModuleEntityLib.pack(address(comprehensiveModule), uint32(ComprehensiveModule.EntityId.VALIDATION)); + + bytes4[] memory validationSelectors = new bytes4[](1); + validationSelectors[0] = ComprehensiveModule.foo.selector; + + vm.startPrank(address(entryPoint)); + account1.installExecution(address(comprehensiveModule), comprehensiveModule.executionManifest(), ""); + account1.installValidation( + ValidationConfigLib.pack(comprehensiveModuleValidation, false, false, true), + validationSelectors, + "", + new bytes[](0) + ); + vm.stopPrank(); + } + + function test_selfCallFails_userOp() public { + // Uses global validation + _runUserOp( + abi.encodeCall(ComprehensiveModule.foo, ()), + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSelector( + ModularAccount.ValidationFunctionMissing.selector, ComprehensiveModule.foo.selector + ) + ) + ); + } + + function test_selfCallFails_execUserOp() public { + // Uses global validation + _runUserOp( + abi.encodePacked(IAccountExecute.executeUserOp.selector, abi.encodeCall(ComprehensiveModule.foo, ())), + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSelector( + ModularAccount.ValidationFunctionMissing.selector, ComprehensiveModule.foo.selector + ) + ) + ); + } + + function test_selfCallFails_runtime() public { + // Uses global validation + _runtimeCall( + abi.encodeCall(ComprehensiveModule.foo, ()), + abi.encodeWithSelector( + ModularAccount.ValidationFunctionMissing.selector, ComprehensiveModule.foo.selector + ) + ); + } + + function test_selfCallPrivilegeEscalation_prevented_userOp() public { + // Using global validation, self-call bypasses custom validation needed for ComprehensiveModule.foo + _runUserOp( + abi.encodeCall( + ModularAccount.execute, (address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())) + ), + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSelector(ModularAccount.SelfCallRecursionDepthExceeded.selector) + ) + ); + + Call[] memory calls = new Call[](1); + calls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); + + _runUserOp( + abi.encodeCall(IModularAccount.executeBatch, (calls)), + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSelector( + ModularAccount.ValidationFunctionMissing.selector, ComprehensiveModule.foo.selector + ) + ) + ); + } + + function test_selfCallPrivilegeEscalation_prevented_execUserOp() public { + // Using global validation, self-call bypasses custom validation needed for ComprehensiveModule.foo + _runUserOp( + abi.encodePacked( + IAccountExecute.executeUserOp.selector, + abi.encodeCall( + ModularAccount.execute, (address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())) + ) + ), + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSelector(ModularAccount.SelfCallRecursionDepthExceeded.selector) + ) + ); + + Call[] memory calls = new Call[](1); + calls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); + + _runUserOp( + abi.encodePacked( + IAccountExecute.executeUserOp.selector, abi.encodeCall(IModularAccount.executeBatch, (calls)) + ), + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSelector( + ModularAccount.ValidationFunctionMissing.selector, ComprehensiveModule.foo.selector + ) + ) + ); + } + + function test_selfCallPrivilegeEscalation_prevented_runtime() public { + // Using global validation, self-call bypasses custom validation needed for ComprehensiveModule.foo + _runtimeCall( + abi.encodeCall( + ModularAccount.execute, (address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())) + ), + abi.encodeWithSelector(ModularAccount.SelfCallRecursionDepthExceeded.selector) + ); + + Call[] memory calls = new Call[](1); + calls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); + + _runtimeExecBatchExpFail( + calls, + abi.encodeWithSelector( + ModularAccount.ValidationFunctionMissing.selector, ComprehensiveModule.foo.selector + ) + ); + } + + function test_batchAction_allowed_userOp() public { + _enableBatchValidation(); + + Call[] memory calls = new Call[](2); + calls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); + calls[1] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); + + PackedUserOperation memory userOp = + _generateUserOpWithComprehensiveModuleValidation(abi.encodeCall(IModularAccount.executeBatch, (calls))); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + vm.expectCall(address(comprehensiveModule), abi.encodeCall(ComprehensiveModule.foo, ()), 2); + entryPoint.handleOps(userOps, beneficiary); + } + + function test_batchAction_allowed_execUserOp() public { + _enableBatchValidation(); + + Call[] memory calls = new Call[](2); + calls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); + calls[1] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); + + PackedUserOperation memory userOp = _generateUserOpWithComprehensiveModuleValidation( + abi.encodePacked( + IAccountExecute.executeUserOp.selector, abi.encodeCall(IModularAccount.executeBatch, (calls)) + ) + ); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + vm.expectCall(address(comprehensiveModule), abi.encodeCall(ComprehensiveModule.foo, ()), 2); + entryPoint.handleOps(userOps, beneficiary); + } + + function test_batchAction_allowed_runtime() public { + _enableBatchValidation(); + + Call[] memory calls = new Call[](2); + calls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); + calls[1] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); + + vm.expectCall(address(comprehensiveModule), abi.encodeCall(ComprehensiveModule.foo, ()), 2); + account1.executeWithAuthorization( + abi.encodeCall(IModularAccount.executeBatch, (calls)), + _encodeSignature(comprehensiveModuleValidation, SELECTOR_ASSOCIATED_VALIDATION, "") + ); + } + + function test_recursiveDepthCapped_userOp() public { + _enableBatchValidation(); + + Call[] memory innerCalls = new Call[](1); + innerCalls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); + + Call[] memory outerCalls = new Call[](1); + outerCalls[0] = Call(address(account1), 0, abi.encodeCall(IModularAccount.executeBatch, (innerCalls))); + + PackedUserOperation memory userOp = _generateUserOpWithComprehensiveModuleValidation( + abi.encodeCall(IModularAccount.executeBatch, (outerCalls)) + ); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + vm.expectRevert( + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSelector(ModularAccount.SelfCallRecursionDepthExceeded.selector) + ) + ); + entryPoint.handleOps(userOps, beneficiary); + } + + function test_recursiveDepthCapped_execUserOp() public { + _enableBatchValidation(); + + Call[] memory innerCalls = new Call[](1); + innerCalls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); + + Call[] memory outerCalls = new Call[](1); + outerCalls[0] = Call(address(account1), 0, abi.encodeCall(IModularAccount.executeBatch, (innerCalls))); + + PackedUserOperation memory userOp = _generateUserOpWithComprehensiveModuleValidation( + abi.encodePacked( + IAccountExecute.executeUserOp.selector, abi.encodeCall(IModularAccount.executeBatch, (outerCalls)) + ) + ); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + vm.expectRevert( + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSelector(ModularAccount.SelfCallRecursionDepthExceeded.selector) + ) + ); + entryPoint.handleOps(userOps, beneficiary); + } + + function test_recursiveDepthCapped_runtime() public { + _enableBatchValidation(); + + Call[] memory innerCalls = new Call[](1); + innerCalls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); + + Call[] memory outerCalls = new Call[](1); + outerCalls[0] = Call(address(account1), 0, abi.encodeCall(IModularAccount.executeBatch, (innerCalls))); + + vm.expectRevert(abi.encodeWithSelector(ModularAccount.SelfCallRecursionDepthExceeded.selector)); + account1.executeWithAuthorization( + abi.encodeCall(IModularAccount.executeBatch, (outerCalls)), + _encodeSignature(comprehensiveModuleValidation, SELECTOR_ASSOCIATED_VALIDATION, "") + ); + } + + function _enableBatchValidation() internal { + // Extend ComprehensiveModule's validation function to also validate `executeBatch`, to allow the + // self-call. + + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = IModularAccount.executeBatch.selector; + + vm.prank(owner1); + account1.executeWithAuthorization( + abi.encodeCall( + ModularAccount.installValidation, + ( + ValidationConfigLib.pack(comprehensiveModuleValidation, false, false, true), + selectors, + "", + new bytes[](0) + ) + ), + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, "") + ); + } + + function _generateUserOpWithComprehensiveModuleValidation(bytes memory callData) + internal + view + returns (PackedUserOperation memory) + { + uint256 nonce = entryPoint.getNonce(address(account1), 0); + return PackedUserOperation({ + sender: address(account1), + nonce: nonce, + initCode: hex"", + callData: callData, + accountGasLimits: _encodeGas(VERIFICATION_GAS_LIMIT, CALL_GAS_LIMIT), + preVerificationGas: 0, + gasFees: _encodeGas(1, 1), + paymasterAndData: hex"", + signature: _encodeSignature( + comprehensiveModuleValidation, + SELECTOR_ASSOCIATED_VALIDATION, + // Comprehensive module's validation function doesn't actually check anything, so we don't need to + // sign anything. + "" + ) + }); + } +} diff --git a/test/account/ValidationIntersection.t.sol b/test/account/ValidationIntersection.t.sol new file mode 100644 index 00000000..72044ab0 --- /dev/null +++ b/test/account/ValidationIntersection.t.sol @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; + +import {ModularAccount} from "../../src/account/ModularAccount.sol"; +import {HookConfigLib} from "../../src/helpers/HookConfigLib.sol"; +import {ModuleEntity, ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; +import {ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; + +import { + MockBaseUserOpValidationModule, + MockUserOpValidation1HookModule, + MockUserOpValidation2HookModule, + MockUserOpValidationModule +} from "../mocks/modules/ValidationModuleMocks.sol"; +import {AccountTestBase} from "../utils/AccountTestBase.sol"; + +contract ValidationIntersectionTest is AccountTestBase { + uint256 internal constant _SIG_VALIDATION_FAILED = 1; + + MockUserOpValidationModule public noHookModule; + MockUserOpValidation1HookModule public oneHookModule; + MockUserOpValidation2HookModule public twoHookModule; + + ModuleEntity public noHookValidation; + ModuleEntity public oneHookValidation; + ModuleEntity public twoHookValidation; + + function setUp() public { + noHookModule = new MockUserOpValidationModule(); + oneHookModule = new MockUserOpValidation1HookModule(); + twoHookModule = new MockUserOpValidation2HookModule(); + + noHookValidation = ModuleEntityLib.pack({ + addr: address(noHookModule), + entityId: uint32(MockBaseUserOpValidationModule.EntityId.USER_OP_VALIDATION) + }); + + oneHookValidation = ModuleEntityLib.pack({ + addr: address(oneHookModule), + entityId: uint32(MockBaseUserOpValidationModule.EntityId.USER_OP_VALIDATION) + }); + + twoHookValidation = ModuleEntityLib.pack({ + addr: address(twoHookModule), + entityId: uint32(MockBaseUserOpValidationModule.EntityId.USER_OP_VALIDATION) + }); + + bytes4[] memory validationSelectors = new bytes4[](1); + validationSelectors[0] = MockUserOpValidationModule.foo.selector; + + vm.startPrank(address(entryPoint)); + // Install noHookValidation + account1.installValidation( + ValidationConfigLib.pack(noHookValidation, true, true, true), + validationSelectors, + bytes(""), + new bytes[](0) + ); + + // Install oneHookValidation + validationSelectors[0] = MockUserOpValidation1HookModule.bar.selector; + bytes[] memory hooks = new bytes[](1); + hooks[0] = abi.encodePacked( + HookConfigLib.packValidationHook( + address(oneHookModule), uint32(MockBaseUserOpValidationModule.EntityId.PRE_VALIDATION_HOOK_1) + ) + ); + account1.installValidation( + ValidationConfigLib.pack(oneHookValidation, true, true, true), validationSelectors, bytes(""), hooks + ); + + // Install twoHookValidation + validationSelectors[0] = MockUserOpValidation2HookModule.baz.selector; + hooks = new bytes[](2); + hooks[0] = abi.encodePacked( + HookConfigLib.packValidationHook( + address(twoHookModule), uint32(MockBaseUserOpValidationModule.EntityId.PRE_VALIDATION_HOOK_1) + ) + ); + hooks[1] = abi.encodePacked( + HookConfigLib.packValidationHook( + address(twoHookModule), uint32(MockBaseUserOpValidationModule.EntityId.PRE_VALIDATION_HOOK_2) + ) + ); + account1.installValidation( + ValidationConfigLib.pack(twoHookValidation, true, true, true), validationSelectors, bytes(""), hooks + ); + vm.stopPrank(); + } + + function testFuzz_validationIntersect_single(uint256 validationData) public { + noHookModule.setValidationData(validationData); + + PackedUserOperation memory userOp; + userOp.callData = bytes.concat(noHookModule.foo.selector); + userOp.signature = _encodeSignature(noHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); + bytes32 uoHash = entryPoint.getUserOpHash(userOp); + + vm.prank(address(entryPoint)); + uint256 returnedValidationData = account1.validateUserOp(userOp, uoHash, 1 wei); + + assertEq(returnedValidationData, validationData); + } + + function test_validationIntersect_authorizer_sigfail_validationFunction() public { + oneHookModule.setValidationData( + _SIG_VALIDATION_FAILED, + 0 // returns OK + ); + + PackedUserOperation memory userOp; + userOp.callData = bytes.concat(oneHookModule.bar.selector); + userOp.signature = _encodeSignature(oneHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); + bytes32 uoHash = entryPoint.getUserOpHash(userOp); + + vm.prank(address(entryPoint)); + uint256 returnedValidationData = account1.validateUserOp(userOp, uoHash, 1 wei); + + // Down-cast to only check the authorizer + assertEq(uint160(returnedValidationData), _SIG_VALIDATION_FAILED); + } + + function test_validationIntersect_authorizer_sigfail_hook() public { + oneHookModule.setValidationData( + 0, // returns OK + _SIG_VALIDATION_FAILED + ); + + PackedUserOperation memory userOp; + userOp.callData = bytes.concat(oneHookModule.bar.selector); + userOp.signature = _encodeSignature(oneHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); + bytes32 uoHash = entryPoint.getUserOpHash(userOp); + + vm.prank(address(entryPoint)); + uint256 returnedValidationData = account1.validateUserOp(userOp, uoHash, 1 wei); + + // Down-cast to only check the authorizer + assertEq(uint160(returnedValidationData), _SIG_VALIDATION_FAILED); + } + + function test_validationIntersect_timeBounds_intersect_1() public { + uint48 start1 = uint48(10); + uint48 end1 = uint48(20); + + uint48 start2 = uint48(15); + uint48 end2 = uint48(25); + + oneHookModule.setValidationData( + _packValidationRes(address(0), start1, end1), _packValidationRes(address(0), start2, end2) + ); + + PackedUserOperation memory userOp; + userOp.callData = bytes.concat(oneHookModule.bar.selector); + userOp.signature = _encodeSignature(oneHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); + bytes32 uoHash = entryPoint.getUserOpHash(userOp); + + vm.prank(address(entryPoint)); + uint256 returnedValidationData = account1.validateUserOp(userOp, uoHash, 1 wei); + + assertEq(returnedValidationData, _packValidationRes(address(0), start2, end1)); + } + + function test_validationIntersect_timeBounds_intersect_2() public { + uint48 start1 = uint48(10); + uint48 end1 = uint48(20); + + uint48 start2 = uint48(15); + uint48 end2 = uint48(25); + + oneHookModule.setValidationData( + _packValidationRes(address(0), start2, end2), _packValidationRes(address(0), start1, end1) + ); + + PackedUserOperation memory userOp; + userOp.callData = bytes.concat(oneHookModule.bar.selector); + userOp.signature = _encodeSignature(oneHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); + bytes32 uoHash = entryPoint.getUserOpHash(userOp); + + vm.prank(address(entryPoint)); + uint256 returnedValidationData = account1.validateUserOp(userOp, uoHash, 1 wei); + + assertEq(returnedValidationData, _packValidationRes(address(0), start2, end1)); + } + + function test_validationIntersect_revert_unexpectedAuthorizer() public { + address badAuthorizer = makeAddr("badAuthorizer"); + + oneHookModule.setValidationData( + 0, // returns OK + uint256(uint160(badAuthorizer)) // returns an aggregator, which preValidation hooks are not allowed to + // do. + ); + + PackedUserOperation memory userOp; + userOp.callData = bytes.concat(oneHookModule.bar.selector); + userOp.signature = _encodeSignature(oneHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); + bytes32 uoHash = entryPoint.getUserOpHash(userOp); + + vm.prank(address(entryPoint)); + vm.expectRevert( + abi.encodeWithSelector( + ModularAccount.UnexpectedAggregator.selector, + address(oneHookModule), + MockBaseUserOpValidationModule.EntityId.PRE_VALIDATION_HOOK_1, + badAuthorizer + ) + ); + account1.validateUserOp(userOp, uoHash, 1 wei); + } + + function test_validationIntersect_validAuthorizer() public { + address goodAuthorizer = makeAddr("goodAuthorizer"); + + oneHookModule.setValidationData( + uint256(uint160(goodAuthorizer)), // returns a valid aggregator + 0 // returns OK + ); + + PackedUserOperation memory userOp; + userOp.callData = bytes.concat(oneHookModule.bar.selector); + userOp.signature = _encodeSignature(oneHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); + bytes32 uoHash = entryPoint.getUserOpHash(userOp); + + vm.prank(address(entryPoint)); + uint256 returnedValidationData = account1.validateUserOp(userOp, uoHash, 1 wei); + + assertEq(address(uint160(returnedValidationData)), goodAuthorizer); + } + + function test_validationIntersect_authorizerAndTimeRange() public { + uint48 start1 = uint48(10); + uint48 end1 = uint48(20); + + uint48 start2 = uint48(15); + uint48 end2 = uint48(25); + + address goodAuthorizer = makeAddr("goodAuthorizer"); + + oneHookModule.setValidationData( + _packValidationRes(goodAuthorizer, start1, end1), _packValidationRes(address(0), start2, end2) + ); + + PackedUserOperation memory userOp; + userOp.callData = bytes.concat(oneHookModule.bar.selector); + userOp.signature = _encodeSignature(oneHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); + bytes32 uoHash = entryPoint.getUserOpHash(userOp); + + vm.prank(address(entryPoint)); + uint256 returnedValidationData = account1.validateUserOp(userOp, uoHash, 1 wei); + + assertEq(returnedValidationData, _packValidationRes(goodAuthorizer, start2, end1)); + } + + function test_validationIntersect_multiplePreValidationHooksIntersect() public { + uint48 start1 = uint48(10); + uint48 end1 = uint48(20); + + uint48 start2 = uint48(15); + uint48 end2 = uint48(25); + + twoHookModule.setValidationData( + 0, // returns OK + _packValidationRes(address(0), start1, end1), + _packValidationRes(address(0), start2, end2) + ); + + PackedUserOperation memory userOp; + userOp.callData = bytes.concat(twoHookModule.baz.selector); + userOp.signature = _encodeSignature(twoHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); + bytes32 uoHash = entryPoint.getUserOpHash(userOp); + + vm.prank(address(entryPoint)); + uint256 returnedValidationData = account1.validateUserOp(userOp, uoHash, 1 wei); + + assertEq(returnedValidationData, _packValidationRes(address(0), start2, end1)); + } + + function test_validationIntersect_multiplePreValidationHooksSigFail() public { + twoHookModule.setValidationData( + 0, // returns OK + 0, // returns OK + _SIG_VALIDATION_FAILED + ); + + PackedUserOperation memory userOp; + userOp.callData = bytes.concat(twoHookModule.baz.selector); + + userOp.signature = _encodeSignature(twoHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); + bytes32 uoHash = entryPoint.getUserOpHash(userOp); + + vm.prank(address(entryPoint)); + uint256 returnedValidationData = account1.validateUserOp(userOp, uoHash, 1 wei); + + // Down-cast to only check the authorizer + assertEq(uint160(returnedValidationData), _SIG_VALIDATION_FAILED); + } + + function _unpackValidationData(uint256 validationData) + internal + pure + returns (address authorizer, uint48 validAfter, uint48 validUntil) + { + authorizer = address(uint160(validationData)); + validUntil = uint48(validationData >> 160); + if (validUntil == 0) { + validUntil = type(uint48).max; + } + validAfter = uint48(validationData >> (48 + 160)); + } + + function _packValidationRes(address authorizer, uint48 validAfter, uint48 validUntil) + internal + pure + returns (uint256) + { + return uint160(authorizer) | (uint256(validUntil) << 160) | (uint256(validAfter) << (160 + 48)); + } + + function _intersectTimeRange(uint48 validafter1, uint48 validuntil1, uint48 validafter2, uint48 validuntil2) + internal + pure + returns (uint48 validAfter, uint48 validUntil) + { + if (validafter1 < validafter2) { + validAfter = validafter2; + } else { + validAfter = validafter1; + } + if (validuntil1 > validuntil2) { + validUntil = validuntil2; + } else { + validUntil = validuntil1; + } + } +} diff --git a/test/mocks/module/ComprehensiveModule.sol b/test/mocks/modules/ComprehensiveModule.sol similarity index 100% rename from test/mocks/module/ComprehensiveModule.sol rename to test/mocks/modules/ComprehensiveModule.sol diff --git a/test/mocks/modules/DirectCallModule.sol b/test/mocks/modules/DirectCallModule.sol new file mode 100644 index 00000000..6327b366 --- /dev/null +++ b/test/mocks/modules/DirectCallModule.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import {IExecutionHookModule} from "@erc-6900/reference-implementation/interfaces/IExecutionHookModule.sol"; +import {IModularAccount} from "@erc-6900/reference-implementation/interfaces/IModularAccount.sol"; +import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol"; + +import {BaseModule} from "../../../src/modules/BaseModule.sol"; + +contract DirectCallModule is BaseModule, IExecutionHookModule { + bool public preHookRan = false; + bool public postHookRan = false; + + function onInstall(bytes calldata) external override {} + + function onUninstall(bytes calldata) external override {} + + function directCall() external returns (bytes memory) { + return IModularAccount(msg.sender).execute(address(this), 0, abi.encodeCall(this.getData, ())); + } + + function getData() external pure returns (bytes memory) { + return hex"04546b"; + } + + function moduleId() external pure returns (string memory) { + return "erc6900.direct-call-module.1.0.0"; + } + + function preExecutionHook(uint32, address sender, uint256, bytes calldata) + external + override + returns (bytes memory) + { + require(sender == address(this), "mock direct call pre execution hook failed"); + preHookRan = true; + return abi.encode(keccak256(hex"04546b")); + } + + function postExecutionHook(uint32, bytes calldata preExecHookData) external override { + require( + abi.decode(preExecHookData, (bytes32)) == keccak256(hex"04546b"), + "mock direct call post execution hook failed" + ); + postHookRan = true; + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(BaseModule, IERC165) + returns (bool) + { + return interfaceId == type(IExecutionHookModule).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/test/mocks/modules/MockAccessControlHookModule.sol b/test/mocks/modules/MockAccessControlHookModule.sol new file mode 100644 index 00000000..8cbfe92d --- /dev/null +++ b/test/mocks/modules/MockAccessControlHookModule.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import {IModularAccount} from "@erc-6900/reference-implementation/interfaces/IModularAccount.sol"; +import {IValidationHookModule} from "@erc-6900/reference-implementation/interfaces/IValidationHookModule.sol"; +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; +import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol"; + +import {BaseModule} from "../../../src/modules/BaseModule.sol"; + +// A pre validaiton hook module that uses per-hook data. +// This example enforces that the target of an `execute` call must only be the previously specified address. +// This is just a mock - it does not enforce this over `executeBatch` and other methods of making calls, and should +// not be used in production.. +contract MockAccessControlHookModule is IValidationHookModule, BaseModule { + mapping(uint32 entityId => mapping(address account => address allowedTarget)) public allowedTargets; + + function onInstall(bytes calldata data) external override { + (uint32 entityId, address allowedTarget) = abi.decode(data, (uint32, address)); + allowedTargets[entityId][msg.sender] = allowedTarget; + } + + function onUninstall(bytes calldata data) external override { + uint32 entityId = abi.decode(data, (uint32)); + delete allowedTargets[entityId][msg.sender]; + } + + function preUserOpValidationHook(uint32 entityId, PackedUserOperation calldata userOp, bytes32) + external + view + override + returns (uint256) + { + if (bytes4(userOp.callData[:4]) == IModularAccount.execute.selector) { + address target = abi.decode(userOp.callData[4:36], (address)); + + // Simulate a merkle proof - require that the target address is also provided in the signature + address proof = address(bytes20(userOp.signature)); + require(proof == target, "Proof doesn't match target"); + require(target == allowedTargets[entityId][msg.sender], "Target not allowed"); + return 0; + } + + revert("Unsupported method"); + } + + function preRuntimeValidationHook( + uint32 entityId, + address, + uint256, + bytes calldata data, + bytes calldata authorization + ) external view override { + if (bytes4(data[:4]) == IModularAccount.execute.selector) { + address target = abi.decode(data[4:36], (address)); + + // Simulate a merkle proof - require that the target address is also provided in the authorization + // data + address proof = address(bytes20(authorization)); + require(proof == target, "Proof doesn't match target"); + require(target == allowedTargets[entityId][msg.sender], "Target not allowed"); + + return; + } + + revert("Unsupported method"); + } + + function preSignatureValidationHook(uint32, address, bytes32 hash, bytes calldata signature) + external + pure + override + { + // Simulates some signature checking by requiring a preimage of the hash. + + require(keccak256(signature) == hash, "Preimage not provided"); + + return; + } + + function moduleId() external pure returns (string memory) { + return "erc6900.mock-access-control-hook-module.1.0.0"; + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(BaseModule, IERC165) + returns (bool) + { + return interfaceId == type(IValidationHookModule).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/test/mocks/module/MockModule.sol b/test/mocks/modules/MockModule.sol similarity index 100% rename from test/mocks/module/MockModule.sol rename to test/mocks/modules/MockModule.sol diff --git a/test/mocks/modules/PermittedCallMocks.sol b/test/mocks/modules/PermittedCallMocks.sol new file mode 100644 index 00000000..885329d4 --- /dev/null +++ b/test/mocks/modules/PermittedCallMocks.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import { + ExecutionManifest, + IExecutionModule, + ManifestExecutionFunction +} from "@erc-6900/reference-implementation/interfaces/IExecutionModule.sol"; + +import {BaseModule} from "../../../src/modules/BaseModule.sol"; +import {ResultCreatorModule} from "./ReturnDataModuleMocks.sol"; + +contract PermittedCallerModule is IExecutionModule, BaseModule { + function onInstall(bytes calldata) external override {} + + function onUninstall(bytes calldata) external override {} + + function executionManifest() external pure override returns (ExecutionManifest memory) { + ExecutionManifest memory manifest; + + manifest.executionFunctions = new ManifestExecutionFunction[](2); + manifest.executionFunctions[0].executionSelector = this.usePermittedCallAllowed.selector; + manifest.executionFunctions[1].executionSelector = this.usePermittedCallNotAllowed.selector; + + for (uint256 i = 0; i < manifest.executionFunctions.length; i++) { + manifest.executionFunctions[i].skipRuntimeValidation = true; + } + + return manifest; + } + + function moduleId() external pure returns (string memory) { + return "erc6900.permitted-caller-module.1.0.0"; + } + + // The manifest requested access to use the module-defined method "foo" + function usePermittedCallAllowed() external view returns (bytes memory) { + return abi.encode(ResultCreatorModule(msg.sender).foo()); + } + + // The manifest has not requested access to use the module-defined method "bar", so this should revert. + function usePermittedCallNotAllowed() external view returns (bytes memory) { + return abi.encode(ResultCreatorModule(msg.sender).bar()); + } +} diff --git a/test/mocks/modules/ReturnDataModuleMocks.sol b/test/mocks/modules/ReturnDataModuleMocks.sol new file mode 100644 index 00000000..1d6e41db --- /dev/null +++ b/test/mocks/modules/ReturnDataModuleMocks.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; + +import { + ExecutionManifest, + IExecutionModule, + ManifestExecutionFunction +} from "@erc-6900/reference-implementation/interfaces/IExecutionModule.sol"; +import {IModularAccount} from "@erc-6900/reference-implementation/interfaces/IModularAccount.sol"; +import {IValidationModule} from "@erc-6900/reference-implementation/interfaces/IValidationModule.sol"; + +import {DIRECT_CALL_VALIDATION_ENTITYID} from "../../../src/helpers/Constants.sol"; + +import {BaseModule} from "../../../src/modules/BaseModule.sol"; + +contract RegularResultContract { + function foo() external pure returns (bytes32) { + return keccak256("bar"); + } + + function bar() external pure returns (bytes32) { + return keccak256("foo"); + } +} + +contract ResultCreatorModule is IExecutionModule, BaseModule { + function onInstall(bytes calldata) external override {} + + function onUninstall(bytes calldata) external override {} + + function foo() external pure returns (bytes32) { + return keccak256("bar"); + } + + function bar() external pure returns (bytes32) { + return keccak256("foo"); + } + + function executionManifest() external pure override returns (ExecutionManifest memory) { + ExecutionManifest memory manifest; + + manifest.executionFunctions = new ManifestExecutionFunction[](2); + manifest.executionFunctions[0] = ManifestExecutionFunction({ + executionSelector: this.foo.selector, + skipRuntimeValidation: true, + allowGlobalValidation: false + }); + manifest.executionFunctions[1] = ManifestExecutionFunction({ + executionSelector: this.bar.selector, + skipRuntimeValidation: false, + allowGlobalValidation: false + }); + + return manifest; + } + + function moduleId() external pure returns (string memory) { + return "erc6900.result-creator-module.1.0.0"; + } +} + +contract ResultConsumerModule is IExecutionModule, BaseModule, IValidationModule { + ResultCreatorModule public immutable RESULT_CREATOR; + RegularResultContract public immutable REGULAR_RESULT_CONTRACT; + + error NotAuthorized(); + + constructor(ResultCreatorModule _resultCreator, RegularResultContract _regularResultContract) { + RESULT_CREATOR = _resultCreator; + REGULAR_RESULT_CONTRACT = _regularResultContract; + } + + // Validation function implementations. We only care about the runtime validation function, to authorize + // itself. + + function validateUserOp(uint32, PackedUserOperation calldata, bytes32) external pure returns (uint256) { + revert NotImplemented(); + } + + function validateRuntime(address, uint32, address sender, uint256, bytes calldata, bytes calldata) + external + view + { + if (sender != address(this)) { + revert NotAuthorized(); + } + } + + function validateSignature(address, uint32, address, bytes32, bytes calldata) external pure returns (bytes4) { + revert NotImplemented(); + } + + // Check the return data through the fallback + function checkResultFallback(bytes32 expected) external view returns (bool) { + // This result should be allowed based on the manifest permission request + bytes32 actual = ResultCreatorModule(msg.sender).foo(); + + return actual == expected; + } + + // Check the return data through the execute with authorization case + function checkResultExecuteWithAuthorization(address target, bytes32 expected) external returns (bool) { + // This result should be allowed based on the manifest permission request + bytes memory returnData = IModularAccount(msg.sender).executeWithAuthorization( + abi.encodeCall(IModularAccount.execute, (target, 0, abi.encodeCall(RegularResultContract.foo, ()))), + abi.encodePacked(this, DIRECT_CALL_VALIDATION_ENTITYID, uint8(0), uint32(1), uint8(255)) // Validation + // function of self, + // selector-associated, with no auth data + ); + + bytes32 actual = abi.decode(abi.decode(returnData, (bytes)), (bytes32)); + + return actual == expected; + } + + function onInstall(bytes calldata) external override {} + + function onUninstall(bytes calldata) external override {} + + function executionManifest() external pure override returns (ExecutionManifest memory) { + ExecutionManifest memory manifest; + + manifest.executionFunctions = new ManifestExecutionFunction[](2); + manifest.executionFunctions[0] = ManifestExecutionFunction({ + executionSelector: this.checkResultFallback.selector, + skipRuntimeValidation: true, + allowGlobalValidation: false + }); + manifest.executionFunctions[1] = ManifestExecutionFunction({ + executionSelector: this.checkResultExecuteWithAuthorization.selector, + skipRuntimeValidation: true, + allowGlobalValidation: false + }); + + return manifest; + } + + function moduleId() external pure returns (string memory) { + return "erc6900.result-consumer-module.1.0.0"; + } +} diff --git a/test/mocks/modules/ValidationModuleMocks.sol b/test/mocks/modules/ValidationModuleMocks.sol new file mode 100644 index 00000000..53367621 --- /dev/null +++ b/test/mocks/modules/ValidationModuleMocks.sol @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; + +import { + ExecutionManifest, + IExecutionModule, + ManifestExecutionFunction +} from "@erc-6900/reference-implementation/interfaces/IExecutionModule.sol"; +import {IValidationHookModule} from "@erc-6900/reference-implementation/interfaces/IValidationHookModule.sol"; +import {IValidationModule} from "@erc-6900/reference-implementation/interfaces/IValidationModule.sol"; + +import {BaseModule} from "../../../src/modules/BaseModule.sol"; + +abstract contract MockBaseUserOpValidationModule is + IExecutionModule, + IValidationModule, + IValidationHookModule, + BaseModule +{ + enum EntityId { + USER_OP_VALIDATION, + PRE_VALIDATION_HOOK_1, + PRE_VALIDATION_HOOK_2 + } + + uint256 internal _userOpValidationFunctionData; + uint256 internal _preUserOpValidationHook1Data; + uint256 internal _preUserOpValidationHook2Data; + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Module interface functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + function onInstall(bytes calldata) external override {} + + function onUninstall(bytes calldata) external override {} + + function preUserOpValidationHook(uint32 entityId, PackedUserOperation calldata, bytes32) + external + view + override + returns (uint256) + { + if (entityId == uint32(EntityId.PRE_VALIDATION_HOOK_1)) { + return _preUserOpValidationHook1Data; + } else if (entityId == uint32(EntityId.PRE_VALIDATION_HOOK_2)) { + return _preUserOpValidationHook2Data; + } + revert NotImplemented(); + } + + function validateUserOp(uint32 entityId, PackedUserOperation calldata, bytes32) + external + view + override + returns (uint256) + { + if (entityId == uint32(EntityId.USER_OP_VALIDATION)) { + return _userOpValidationFunctionData; + } + revert NotImplemented(); + } + + function preSignatureValidationHook(uint32, address, bytes32, bytes calldata) external pure override {} + + function validateSignature(address, uint32, address, bytes32, bytes calldata) + external + pure + override + returns (bytes4) + { + revert NotImplemented(); + } + + function moduleId() external pure returns (string memory) { + return "erc6900.mock-user-op-validation-module.1.0.0"; + } + + // Empty stubs + function preRuntimeValidationHook(uint32, address, uint256, bytes calldata, bytes calldata) + external + pure + override + { + revert NotImplemented(); + } + + function validateRuntime(address, uint32, address, uint256, bytes calldata, bytes calldata) + external + pure + override + { + revert NotImplemented(); + } +} + +contract MockUserOpValidationModule is MockBaseUserOpValidationModule { + function setValidationData(uint256 userOpValidationFunctionData) external { + _userOpValidationFunctionData = userOpValidationFunctionData; + } + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Execution functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + function foo() external {} + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Module interface functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + function executionManifest() external pure override returns (ExecutionManifest memory) { + ExecutionManifest memory manifest; + + manifest.executionFunctions = new ManifestExecutionFunction[](1); + manifest.executionFunctions[0] = ManifestExecutionFunction({ + executionSelector: this.foo.selector, + skipRuntimeValidation: false, + allowGlobalValidation: false + }); + + return manifest; + } +} + +contract MockUserOpValidation1HookModule is MockBaseUserOpValidationModule { + function setValidationData(uint256 userOpValidationFunctionData, uint256 preUserOpValidationHook1Data) + external + { + _userOpValidationFunctionData = userOpValidationFunctionData; + _preUserOpValidationHook1Data = preUserOpValidationHook1Data; + } + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Execution functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + function bar() external {} + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Module interface functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + function executionManifest() external pure override returns (ExecutionManifest memory) { + ExecutionManifest memory manifest; + + manifest.executionFunctions = new ManifestExecutionFunction[](1); + manifest.executionFunctions[0] = ManifestExecutionFunction({ + executionSelector: this.bar.selector, + skipRuntimeValidation: false, + allowGlobalValidation: false + }); + + return manifest; + } +} + +contract MockUserOpValidation2HookModule is MockBaseUserOpValidationModule { + function setValidationData( + uint256 userOpValidationFunctionData, + uint256 preUserOpValidationHook1Data, + uint256 preUserOpValidationHook2Data + ) external { + _userOpValidationFunctionData = userOpValidationFunctionData; + _preUserOpValidationHook1Data = preUserOpValidationHook1Data; + _preUserOpValidationHook2Data = preUserOpValidationHook2Data; + } + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Execution functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + function baz() external {} + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Module interface functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + function executionManifest() external pure override returns (ExecutionManifest memory) { + ExecutionManifest memory manifest; + + manifest.executionFunctions = new ManifestExecutionFunction[](1); + manifest.executionFunctions[0] = ManifestExecutionFunction({ + executionSelector: this.baz.selector, + skipRuntimeValidation: false, + allowGlobalValidation: false + }); + + return manifest; + } +} diff --git a/test/utils/AccountTestBase.sol b/test/utils/AccountTestBase.sol index b147295e..06bb44fb 100644 --- a/test/utils/AccountTestBase.sol +++ b/test/utils/AccountTestBase.sol @@ -10,8 +10,12 @@ import {Call, IModularAccount} from "@erc-6900/reference-implementation/interfac import {AccountFactory} from "../../src/account/AccountFactory.sol"; import {ModularAccount} from "../../src/account/ModularAccount.sol"; import {SemiModularAccount} from "../../src/account/SemiModularAccount.sol"; + +import {DIRECT_CALL_VALIDATION_ENTITYID} from "../../src/helpers/Constants.sol"; import {ModuleEntity, ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; +import {ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; import {SingleSignerValidationModule} from "../../src/modules/validation/SingleSignerValidationModule.sol"; + import {OptimizedTest} from "./OptimizedTest.sol"; import {TEST_DEFAULT_VALIDATION_ENTITY_ID as EXT_CONST_TEST_DEFAULT_VALIDATION_ENTITY_ID} from "./TestConstants.sol"; @@ -77,7 +81,12 @@ abstract contract AccountTestBase is OptimizedTest { factoryOwner ); - account1 = factory.createAccount(owner1, 0, TEST_DEFAULT_VALIDATION_ENTITY_ID); + if (vm.envOr("SMA_TEST", false)) { + account1 = factory.createSemiModularAccount(owner1, 0); + } else { + account1 = factory.createAccount(owner1, 0, TEST_DEFAULT_VALIDATION_ENTITY_ID); + } + vm.deal(address(account1), 100 ether); _signerValidation = @@ -207,6 +216,22 @@ abstract contract AccountTestBase is OptimizedTest { ); } + function _allowTestDirectCalls() internal { + vm.prank(owner1); + account1.executeWithAuthorization( + abi.encodeCall( + account1.installValidation, + ( + ValidationConfigLib.pack(address(this), DIRECT_CALL_VALIDATION_ENTITYID, true, false, false), + new bytes4[](0), + "", + new bytes[](0) + ) + ), + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, "") + ); + } + // helper function to compress 2 gas values into a single bytes32 function _encodeGas(uint256 g1, uint256 g2) internal pure returns (bytes32) { return bytes32(uint256((g1 << 128) + uint128(g2)));