Skip to content

Commit 094605e

Browse files
authored
feat: add interface checks for validations and hooks (#147)
1 parent a1510cd commit 094605e

File tree

9 files changed

+110
-41
lines changed

9 files changed

+110
-41
lines changed

src/account/ModuleManagerInternals.sol

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ import {HookConfigLib} from "../helpers/HookConfigLib.sol";
1010
import {KnownSelectors} from "../helpers/KnownSelectors.sol";
1111
import {ModuleEntityLib} from "../helpers/ModuleEntityLib.sol";
1212
import {ValidationConfigLib} from "../helpers/ValidationConfigLib.sol";
13+
import {IExecutionHookModule} from "../interfaces/IExecutionHookModule.sol";
1314
import {ExecutionManifest, ManifestExecutionHook} from "../interfaces/IExecutionModule.sol";
1415
import {HookConfig, IModularAccount, ModuleEntity, ValidationConfig} from "../interfaces/IModularAccount.sol";
1516
import {IModule} from "../interfaces/IModule.sol";
17+
import {IValidationHookModule} from "../interfaces/IValidationHookModule.sol";
18+
import {IValidationModule} from "../interfaces/IValidationModule.sol";
19+
1620
import {
1721
AccountStorage,
1822
ExecutionData,
@@ -32,11 +36,11 @@ abstract contract ModuleManagerInternals is IModularAccount {
3236
error Erc4337FunctionNotAllowed(bytes4 selector);
3337
error ExecutionFunctionAlreadySet(bytes4 selector);
3438
error IModuleFunctionNotAllowed(bytes4 selector);
39+
error InterfaceNotSupported(address module);
3540
error NativeFunctionNotAllowed(bytes4 selector);
3641
error NullModule();
3742
error PermissionAlreadySet(ModuleEntity validationFunction, HookConfig hookConfig);
3843
error ModuleInstallCallbackFailed(address module, bytes revertReason);
39-
error ModuleInterfaceNotSupported(address module);
4044
error ModuleNotInstalled(address module);
4145
error PreValidationHookLimitExceeded();
4246
error ValidationAlreadySet(bytes4 selector, ModuleEntity validationFunction);
@@ -125,21 +129,17 @@ abstract contract ModuleManagerInternals is IModularAccount {
125129
hooks.remove(toSetValue(hookConfig));
126130
}
127131

128-
function _installExecution(address module, ExecutionManifest calldata manifest, bytes memory moduleInstallData)
129-
internal
130-
{
132+
function _installExecution(
133+
address module,
134+
ExecutionManifest calldata manifest,
135+
bytes calldata moduleInstallData
136+
) internal {
131137
AccountStorage storage _storage = getAccountStorage();
132138

133139
if (module == address(0)) {
134140
revert NullModule();
135141
}
136142

137-
// TODO: do we need this check? Or switch to a non-165 checking function?
138-
// Check that the module supports the IModule interface.
139-
if (!ERC165Checker.supportsInterface(module, type(IModule).interfaceId)) {
140-
revert ModuleInterfaceNotSupported(module);
141-
}
142-
143143
// Update components according to the manifest.
144144
uint256 length = manifest.executionFunctions.length;
145145
for (uint256 i = 0; i < length; ++i) {
@@ -168,18 +168,12 @@ abstract contract ModuleManagerInternals is IModularAccount {
168168
_storage.supportedIfaces[manifest.interfaceIds[i]] += 1;
169169
}
170170

171-
// Initialize the module storage for the account.
172-
// solhint-disable-next-line no-empty-blocks
173-
try IModule(module).onInstall(moduleInstallData) {}
174-
catch {
175-
bytes memory revertReason = collectReturnData();
176-
revert ModuleInstallCallbackFailed(module, revertReason);
177-
}
171+
_onInstall(module, moduleInstallData, type(IModule).interfaceId);
178172

179173
emit ExecutionInstalled(module, manifest);
180174
}
181175

182-
function _uninstallExecution(address module, ExecutionManifest calldata manifest, bytes memory uninstallData)
176+
function _uninstallExecution(address module, ExecutionManifest calldata manifest, bytes calldata uninstallData)
183177
internal
184178
{
185179
AccountStorage storage _storage = getAccountStorage();
@@ -212,19 +206,22 @@ abstract contract ModuleManagerInternals is IModularAccount {
212206
}
213207

214208
// Clear the module storage for the account.
215-
bool onUninstallSuccess = true;
216-
// solhint-disable-next-line no-empty-blocks
217-
try IModule(module).onUninstall(uninstallData) {}
218-
catch {
219-
onUninstallSuccess = false;
220-
}
209+
bool onUninstallSuccess = _onUninstall(module, uninstallData);
221210

222211
emit ExecutionUninstalled(module, onUninstallSuccess, manifest);
223212
}
224213

225-
function _onInstall(address module, bytes calldata data) internal {
214+
function _onInstall(address module, bytes calldata data, bytes4 interfaceId) internal {
226215
if (data.length > 0) {
227-
IModule(module).onInstall(data);
216+
if (!ERC165Checker.supportsInterface(module, interfaceId)) {
217+
revert InterfaceNotSupported(module);
218+
}
219+
// solhint-disable-next-line no-empty-blocks
220+
try IModule(module).onInstall(data) {}
221+
catch {
222+
bytes memory revertReason = collectReturnData();
223+
revert ModuleInstallCallbackFailed(module, revertReason);
224+
}
228225
}
229226
}
230227

@@ -261,12 +258,17 @@ abstract contract ModuleManagerInternals is IModularAccount {
261258
if (_validationData.preValidationHooks.length > MAX_PRE_VALIDATION_HOOKS) {
262259
revert PreValidationHookLimitExceeded();
263260
}
264-
} // Hook is an execution hook
265-
else if (!_validationData.permissionHooks.add(toSetValue(hookConfig))) {
261+
262+
_onInstall(hookConfig.module(), hookData, type(IValidationHookModule).interfaceId);
263+
264+
continue;
265+
}
266+
// Hook is a permission hook
267+
if (!_validationData.permissionHooks.add(toSetValue(hookConfig))) {
266268
revert PermissionAlreadySet(moduleEntity, hookConfig);
267269
}
268270

269-
_onInstall(hookConfig.module(), hookData);
271+
_onInstall(hookConfig.module(), hookData, type(IExecutionHookModule).interfaceId);
270272
}
271273

272274
for (uint256 i = 0; i < selectors.length; ++i) {
@@ -279,7 +281,7 @@ abstract contract ModuleManagerInternals is IModularAccount {
279281
_validationData.isGlobal = validationConfig.isGlobal();
280282
_validationData.isSignatureValidation = validationConfig.isSignatureValidation();
281283

282-
_onInstall(validationConfig.module(), installData);
284+
_onInstall(validationConfig.module(), installData, type(IValidationModule).interfaceId);
283285
emit ValidationInstalled(validationConfig.module(), validationConfig.entityId());
284286
}
285287

src/modules/ERC20TokenLimitModule.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ contract ERC20TokenLimitModule is BaseModule, IExecutionHookModule {
120120

121121
/// @inheritdoc BaseModule
122122
function supportsInterface(bytes4 interfaceId) public view override(BaseModule, IERC165) returns (bool) {
123-
return super.supportsInterface(interfaceId);
123+
return interfaceId == type(IExecutionHookModule).interfaceId || super.supportsInterface(interfaceId);
124124
}
125125

126126
function _decrementLimit(uint32 entityId, address token, bytes memory innerCalldata) internal {

src/modules/permissionhooks/AllowlistModule.sol

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pragma solidity ^0.8.25;
33

44
import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
5+
import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol";
56

67
import {IModule} from "../../interfaces/IModule.sol";
78

@@ -118,6 +119,16 @@ contract AllowlistModule is IValidationHookModule, BaseModule {
118119
}
119120
}
120121

122+
function supportsInterface(bytes4 interfaceId)
123+
public
124+
view
125+
virtual
126+
override(BaseModule, IERC165)
127+
returns (bool)
128+
{
129+
return interfaceId == type(IValidationHookModule).interfaceId || super.supportsInterface(interfaceId);
130+
}
131+
121132
function _checkCallPermission(uint32 entityId, address account, address target, bytes memory data)
122133
internal
123134
view

src/modules/validation/SingleSignerValidationModule.sol

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
// SPDX-License-Identifier: GPL-3.0
22
pragma solidity ^0.8.25;
33

4+
import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
5+
import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol";
6+
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
7+
import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
8+
49
import {IModule} from "../../interfaces/IModule.sol";
510
import {IValidationModule} from "../../interfaces/IValidationModule.sol";
611
import {BaseModule} from "../BaseModule.sol";
712

813
import {ReplaySafeWrapper} from "../ReplaySafeWrapper.sol";
914
import {ISingleSignerValidationModule} from "./ISingleSignerValidationModule.sol";
10-
import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
11-
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
12-
import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
1315

1416
/// @title ECSDA Validation
1517
/// @author ERC-6900 Authors
@@ -115,6 +117,16 @@ contract SingleSignerValidationModule is ISingleSignerValidationModule, ReplaySa
115117
return "erc6900/single-signer-validation-module/1.0.0";
116118
}
117119

120+
function supportsInterface(bytes4 interfaceId)
121+
public
122+
view
123+
virtual
124+
override(BaseModule, IERC165)
125+
returns (bool)
126+
{
127+
return (interfaceId == type(IValidationModule).interfaceId || super.supportsInterface(interfaceId));
128+
}
129+
118130
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
119131
// ┃ Internal / Private functions ┃
120132
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

test/account/AccountExecHooks.t.sol

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,27 +162,27 @@ contract AccountExecHooksTest is AccountTestBase {
162162
mockModule1 = new MockModule(_m1);
163163

164164
vm.expectEmit(true, true, true, true);
165-
emit ReceivedCall(abi.encodeCall(IModule.onInstall, (bytes(""))), 0);
165+
emit ReceivedCall(abi.encodeCall(IModule.onInstall, (bytes("a"))), 0);
166166
vm.expectEmit(true, true, true, true);
167167
emit ExecutionInstalled(address(mockModule1), _m1);
168168

169169
vm.startPrank(address(entryPoint));
170170
account1.installExecution({
171171
module: address(mockModule1),
172172
manifest: mockModule1.executionManifest(),
173-
moduleInstallData: bytes("")
173+
moduleInstallData: bytes("a")
174174
});
175175
vm.stopPrank();
176176
}
177177

178178
function _uninstallExecution(MockModule module) internal {
179179
vm.expectEmit(true, true, true, true);
180-
emit ReceivedCall(abi.encodeCall(IModule.onUninstall, (bytes(""))), 0);
180+
emit ReceivedCall(abi.encodeCall(IModule.onUninstall, (bytes("b"))), 0);
181181
vm.expectEmit(true, true, true, true);
182182
emit ExecutionUninstalled(address(module), true, module.executionManifest());
183183

184184
vm.startPrank(address(entryPoint));
185-
account1.uninstallExecution(address(module), module.executionManifest(), bytes(""));
185+
account1.uninstallExecution(address(module), module.executionManifest(), bytes("b"));
186186
vm.stopPrank();
187187
}
188188
}

test/account/DirectCallsFromModule.t.sol

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {DirectCallModule} from "../mocks/modules/DirectCallModule.sol";
1111

1212
import {AccountTestBase} from "../utils/AccountTestBase.sol";
1313

14+
import {DIRECT_CALL_VALIDATION_ENTITYID} from "../../src/helpers/Constants.sol";
15+
1416
contract DirectCallsFromModuleTest is AccountTestBase {
1517
using ValidationConfigLib for ValidationConfig;
1618

@@ -23,7 +25,7 @@ contract DirectCallsFromModuleTest is AccountTestBase {
2325
_module = new DirectCallModule();
2426
assertFalse(_module.preHookRan());
2527
assertFalse(_module.postHookRan());
26-
_moduleEntity = ModuleEntityLib.pack(address(_module), type(uint32).max);
28+
_moduleEntity = ModuleEntityLib.pack(address(_module), DIRECT_CALL_VALIDATION_ENTITYID);
2729
}
2830

2931
/* -------------------------------------------------------------------------- */
@@ -104,6 +106,25 @@ contract DirectCallsFromModuleTest is AccountTestBase {
104106
account1.execute(address(0), 0, "");
105107
}
106108

109+
function test_directCallsFromEOA() external {
110+
address extraOwner = makeAddr("extraOwner");
111+
112+
bytes4[] memory selectors = new bytes4[](1);
113+
selectors[0] = IModularAccount.execute.selector;
114+
115+
vm.prank(address(entryPoint));
116+
117+
account1.installValidation(
118+
ValidationConfigLib.pack(extraOwner, DIRECT_CALL_VALIDATION_ENTITYID, false, false),
119+
selectors,
120+
"",
121+
new bytes[](0)
122+
);
123+
124+
vm.prank(extraOwner);
125+
account1.execute(makeAddr("dead"), 0, "");
126+
}
127+
107128
/* -------------------------------------------------------------------------- */
108129
/* Internals */
109130
/* -------------------------------------------------------------------------- */

test/account/UpgradeableModularAccount.t.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,12 +288,12 @@ contract UpgradeableModularAccountTest is AccountTestBase {
288288

289289
address badModule = address(1);
290290
vm.expectRevert(
291-
abi.encodeWithSelector(ModuleManagerInternals.ModuleInterfaceNotSupported.selector, address(badModule))
291+
abi.encodeWithSelector(ModuleManagerInternals.InterfaceNotSupported.selector, address(badModule))
292292
);
293293

294294
ExecutionManifest memory m;
295295

296-
account1.installExecution({module: address(badModule), manifest: m, moduleInstallData: ""});
296+
account1.installExecution({module: address(badModule), manifest: m, moduleInstallData: "a"});
297297
}
298298

299299
function test_installExecution_alreadyInstalled() public {

test/mocks/modules/DirectCallModule.sol

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// SPDX-License-Identifier: UNLICENSED
22
pragma solidity ^0.8.19;
33

4+
import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol";
5+
46
import {IExecutionHookModule} from "../../../src/interfaces/IExecutionHookModule.sol";
57
import {IModularAccount} from "../../../src/interfaces/IModularAccount.sol";
68
import {BaseModule} from "../../../src/modules/BaseModule.sol";
@@ -42,4 +44,14 @@ contract DirectCallModule is BaseModule, IExecutionHookModule {
4244
);
4345
postHookRan = true;
4446
}
47+
48+
function supportsInterface(bytes4 interfaceId)
49+
public
50+
view
51+
virtual
52+
override(BaseModule, IERC165)
53+
returns (bool)
54+
{
55+
return interfaceId == type(IExecutionHookModule).interfaceId || super.supportsInterface(interfaceId);
56+
}
4557
}

test/mocks/modules/MockAccessControlHookModule.sol

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pragma solidity ^0.8.25;
33

44
import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
5+
import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol";
56

67
import {IModularAccount} from "../../../src/interfaces/IModularAccount.sol";
78
import {IValidationHookModule} from "../../../src/interfaces/IValidationHookModule.sol";
@@ -86,4 +87,14 @@ contract MockAccessControlHookModule is IValidationHookModule, BaseModule {
8687
function moduleId() external pure returns (string memory) {
8788
return "erc6900/mock-access-control-hook-module/1.0.0";
8889
}
90+
91+
function supportsInterface(bytes4 interfaceId)
92+
public
93+
view
94+
virtual
95+
override(BaseModule, IERC165)
96+
returns (bool)
97+
{
98+
return interfaceId == type(IValidationHookModule).interfaceId || super.supportsInterface(interfaceId);
99+
}
89100
}

0 commit comments

Comments
 (0)