-
Notifications
You must be signed in to change notification settings - Fork 30
Allow direct plugin calls with validation & permission hooks #90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fe5af61
b11b7b4
222243e
e1adaca
1d45b2e
883eff5
691dd31
4a99058
efc111f
2a58118
d614b0e
f0b8321
43a3e5c
e615bc2
b537f07
b3b834f
4aa008c
ae26e5f
a09ffb2
a978cd7
ad4fcb6
d263656
0f04d85
32c39fb
a425c9b
4a29ae3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,20 @@ | ||
| { | ||
| "extends": "solhint:recommended", | ||
| "rules": { | ||
| "func-name-mixedcase": "off", | ||
| "immutable-vars-naming": ["error"], | ||
| "no-unused-import": ["error"], | ||
| "compiler-version": ["error", ">=0.8.19"], | ||
| "custom-errors": "off", | ||
| "func-visibility": ["error", { "ignoreConstructors": true }], | ||
| "max-line-length": ["error", 120], | ||
| "max-states-count": ["warn", 30], | ||
| "modifier-name-mixedcase": ["error"], | ||
| "private-vars-leading-underscore": ["error"], | ||
| "no-inline-assembly": "off", | ||
| "avoid-low-level-calls": "off", | ||
| "one-contract-per-file": "off", | ||
| "no-empty-blocks": "off" | ||
| } | ||
| "extends": "solhint:recommended", | ||
| "rules": { | ||
| "func-name-mixedcase": "off", | ||
| "immutable-vars-naming": ["error"], | ||
| "no-unused-import": ["error"], | ||
| "compiler-version": ["error", ">=0.8.19"], | ||
| "custom-errors": "off", | ||
| "func-visibility": ["error", { "ignoreConstructors": true }], | ||
| "max-line-length": ["error", 120], | ||
| "max-states-count": ["warn", 30], | ||
| "modifier-name-mixedcase": ["error"], | ||
| "private-vars-leading-underscore": ["error"], | ||
| "no-inline-assembly": "off", | ||
| "avoid-low-level-calls": "off", | ||
| "one-contract-per-file": "off", | ||
| "no-empty-blocks": "off", | ||
| "reason-string": ["warn", { "maxLength": 64 }] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,7 @@ import {IPlugin} from "../interfaces/IPlugin.sol"; | |
| import {PluginEntity, ValidationConfig} from "../interfaces/IPluginManager.sol"; | ||
| import {PluginEntityLib} from "../helpers/PluginEntityLib.sol"; | ||
| import {ValidationConfigLib} from "../helpers/ValidationConfigLib.sol"; | ||
| import {ValidationData, getAccountStorage, toSetValue, toPluginEntity} from "./AccountStorage.sol"; | ||
| import {ValidationData, getAccountStorage, toSetValue} from "./AccountStorage.sol"; | ||
| import {ExecutionHook} from "../interfaces/IAccountLoupe.sol"; | ||
|
|
||
| // Temporary additional functions for a user-controlled install flow for validation functions. | ||
|
|
@@ -17,6 +17,7 @@ abstract contract PluginManager2 { | |
|
|
||
| // Index marking the start of the data for the validation function. | ||
| uint8 internal constant _RESERVED_VALIDATION_DATA_INDEX = 255; | ||
| uint32 internal constant _SELF_PERMIT_VALIDATION_FUNCTIONID = type(uint32).max; | ||
|
|
||
| error PreValidationAlreadySet(PluginEntity validationFunction, PluginEntity preValidationFunction); | ||
| error ValidationAlreadySet(bytes4 selector, PluginEntity validationFunction); | ||
|
|
@@ -32,7 +33,7 @@ abstract contract PluginManager2 { | |
| bytes memory permissionHooks | ||
| ) internal { | ||
| ValidationData storage _validationData = | ||
| getAccountStorage().validationData[validationConfig.functionReference()]; | ||
| getAccountStorage().validationData[validationConfig.pluginEntity()]; | ||
|
|
||
| if (preValidationHooks.length > 0) { | ||
| (PluginEntity[] memory preValidationFunctions, bytes[] memory initDatas) = | ||
|
|
@@ -63,7 +64,7 @@ abstract contract PluginManager2 { | |
| ExecutionHook memory permissionFunction = permissionFunctions[i]; | ||
|
|
||
| if (!_validationData.permissionHooks.add(toSetValue(permissionFunction))) { | ||
| revert PermissionAlreadySet(validationConfig.functionReference(), permissionFunction); | ||
| revert PermissionAlreadySet(validationConfig.pluginEntity(), permissionFunction); | ||
| } | ||
|
|
||
| if (initDatas[i].length > 0) { | ||
|
|
@@ -73,19 +74,21 @@ abstract contract PluginManager2 { | |
| } | ||
| } | ||
|
|
||
| _validationData.isGlobal = validationConfig.isGlobal(); | ||
| _validationData.isSignatureValidation = validationConfig.isSignatureValidation(); | ||
|
|
||
| for (uint256 i = 0; i < selectors.length; ++i) { | ||
| bytes4 selector = selectors[i]; | ||
| if (!_validationData.selectors.add(toSetValue(selector))) { | ||
| revert ValidationAlreadySet(selector, validationConfig.functionReference()); | ||
| revert ValidationAlreadySet(selector, validationConfig.pluginEntity()); | ||
| } | ||
| } | ||
|
|
||
| if (installData.length > 0) { | ||
| address plugin = validationConfig.plugin(); | ||
| IPlugin(plugin).onInstall(installData); | ||
| if (validationConfig.entityId() != _SELF_PERMIT_VALIDATION_FUNCTIONID) { | ||
| // Only allow global validations and signature validations if they're not direct-call validations. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this necessary to prevent an access control issue? Or is it just an anti-footgun measure?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's just an anti-footgun, though I've not evaluated the consequences of having global validation for a direct call-- signature validation either.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok sounds good. It might help to support global/sig validation in some very niche cases where the direct call validation is used to create an "owner" EOA, but that seems small enough to ignore for now.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gotcha, happy to revisit this down the line if we see the feature's important! |
||
|
|
||
| _validationData.isGlobal = validationConfig.isGlobal(); | ||
| _validationData.isSignatureValidation = validationConfig.isSignatureValidation(); | ||
| if (installData.length > 0) { | ||
| IPlugin(validationConfig.plugin()).onInstall(installData); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -120,12 +123,12 @@ abstract contract PluginManager2 { | |
|
|
||
| // Clear permission hooks | ||
| EnumerableSet.Bytes32Set storage permissionHooks = _validationData.permissionHooks; | ||
| uint256 i = 0; | ||
| while (permissionHooks.length() > 0) { | ||
| PluginEntity permissionHook = toPluginEntity(permissionHooks.at(0)); | ||
| permissionHooks.remove(toSetValue(permissionHook)); | ||
| (address permissionHookPlugin,) = PluginEntityLib.unpack(permissionHook); | ||
| IPlugin(permissionHookPlugin).onUninstall(permissionHookUninstallDatas[i++]); | ||
| uint256 len = permissionHooks.length(); | ||
| for (uint256 i = 0; i < len; ++i) { | ||
| bytes32 permissionHook = permissionHooks.at(0); | ||
| permissionHooks.remove(permissionHook); | ||
| address permissionHookPlugin = address(uint160(bytes20(permissionHook))); | ||
| IPlugin(permissionHookPlugin).onUninstall(permissionHookUninstallDatas[i]); | ||
| } | ||
| } | ||
| delete _validationData.preValidationHooks; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,22 +78,21 @@ contract UpgradeableModularAccount is | |
| error SignatureValidationInvalid(address plugin, uint32 entityId); | ||
| error UnexpectedAggregator(address plugin, uint32 entityId, address aggregator); | ||
| error UnrecognizedFunction(bytes4 selector); | ||
| error UserOpValidationFunctionMissing(bytes4 selector); | ||
| error ValidationFunctionMissing(bytes4 selector); | ||
| error ValidationDoesNotApply(bytes4 selector, address plugin, uint32 entityId, bool isGlobal); | ||
| error ValidationSignatureSegmentMissing(); | ||
| error SignatureSegmentOutOfOrder(); | ||
|
|
||
| // Wraps execution of a native function with runtime validation and hooks | ||
| // Used for upgradeTo, upgradeToAndCall, execute, executeBatch, installPlugin, uninstallPlugin | ||
| modifier wrapNativeFunction() { | ||
| _checkPermittedCallerIfNotFromEP(); | ||
|
|
||
| PostExecToRun[] memory postExecHooks = | ||
| _doPreHooks(getAccountStorage().selectorData[msg.sig].executionHooks, msg.data); | ||
| (PostExecToRun[] memory postPermissionHooks, PostExecToRun[] memory postExecHooks) = | ||
| _checkPermittedCallerAndAssociatedHooks(); | ||
|
|
||
| _; | ||
|
|
||
| _doCachedPostExecHooks(postExecHooks); | ||
| _doCachedPostExecHooks(postPermissionHooks); | ||
| } | ||
|
|
||
| constructor(IEntryPoint anEntryPoint) { | ||
|
|
@@ -136,7 +135,7 @@ contract UpgradeableModularAccount is | |
| revert UnrecognizedFunction(msg.sig); | ||
| } | ||
|
|
||
| _checkPermittedCallerIfNotFromEP(); | ||
| _checkPermittedCallerAndAssociatedHooks(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if a signer of the account call an execFunction? Would it fail here?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I.e. if a signer, as set in the storage of Alternatively, if an account is only used from an EOA, you could add an EOA itself as an allowed caller by installing the EOA address + the direct call magic value as a validation. But, this wouldn't be usable via user op validation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aren't we executing the pre-execution hooks currently twice in case a permitted caller is calling through fallback? Within
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @0xrubes you are correct... we'll get a fix PR up soon. |
||
|
|
||
| PostExecToRun[] memory postExecHooks; | ||
| // Cache post-exec hooks in memory | ||
|
|
@@ -500,17 +499,7 @@ contract UpgradeableModularAccount is | |
| } else { | ||
| currentAuthData = ""; | ||
| } | ||
|
|
||
| (address hookPlugin, uint32 hookEntityId) = preRuntimeValidationHooks[i].unpack(); | ||
| try IValidationHook(hookPlugin).preRuntimeValidationHook( | ||
| hookEntityId, msg.sender, msg.value, callData, currentAuthData | ||
| ) | ||
| // forgefmt: disable-start | ||
| // solhint-disable-next-line no-empty-blocks | ||
| {} catch (bytes memory revertReason) { | ||
| // forgefmt: disable-end | ||
| revert PreRuntimeValidationHookFailed(hookPlugin, hookEntityId, revertReason); | ||
| } | ||
| _doPreRuntimeValidationHook(preRuntimeValidationHooks[i], callData, currentAuthData); | ||
| } | ||
|
|
||
| if (authSegment.getIndex() != _RESERVED_VALIDATION_DATA_INDEX) { | ||
|
|
@@ -605,9 +594,78 @@ contract UpgradeableModularAccount is | |
| } | ||
| } | ||
|
|
||
| function _doPreRuntimeValidationHook( | ||
| PluginEntity validationHook, | ||
| bytes memory callData, | ||
| bytes memory currentAuthData | ||
| ) internal { | ||
| (address hookPlugin, uint32 hookEntityId) = validationHook.unpack(); | ||
| try IValidationHook(hookPlugin).preRuntimeValidationHook( | ||
| hookEntityId, msg.sender, msg.value, callData, currentAuthData | ||
| ) | ||
| // forgefmt: disable-start | ||
| // solhint-disable-next-line no-empty-blocks | ||
| {} catch (bytes memory revertReason) { | ||
| // forgefmt: disable-end | ||
| revert PreRuntimeValidationHookFailed(hookPlugin, hookEntityId, revertReason); | ||
| } | ||
| } | ||
|
|
||
| // solhint-disable-next-line no-empty-blocks | ||
| function _authorizeUpgrade(address newImplementation) internal override {} | ||
|
|
||
| /** | ||
| * Order of operations: | ||
| * 1. Check if the sender is the entry point, the account itself, or the selector called is public. | ||
| * - Yes: Return an empty array, there are no post-permissionHooks. | ||
| * - No: Continue | ||
| * 2. Check if the called selector (msg.sig) is included in the set of selectors the msg.sender can | ||
| * directly call. | ||
| * - Yes: Continue | ||
| * - No: Revert, the caller is not allowed to call this selector | ||
| * 3. If there are runtime validation hooks associated with this caller-sig combination, run them. | ||
| * 4. Run the pre-permissionHooks associated with this caller-sig combination, and return the | ||
| * post-permissionHooks to run later. | ||
| */ | ||
| function _checkPermittedCallerAndAssociatedHooks() | ||
| internal | ||
| returns (PostExecToRun[] memory, PostExecToRun[] memory) | ||
| { | ||
| AccountStorage storage _storage = getAccountStorage(); | ||
|
|
||
| if ( | ||
| msg.sender == address(_ENTRY_POINT) || msg.sender == address(this) | ||
| || _storage.selectorData[msg.sig].isPublic | ||
| ) { | ||
| return (new PostExecToRun[](0), new PostExecToRun[](0)); | ||
| } | ||
|
|
||
| PluginEntity directCallValidationKey = PluginEntityLib.pack(msg.sender, _SELF_PERMIT_VALIDATION_FUNCTIONID); | ||
|
|
||
| _checkIfValidationAppliesCallData(msg.data, directCallValidationKey, false); | ||
|
|
||
| // Direct call is allowed, run associated permission & validation hooks | ||
|
|
||
| // Validation hooks | ||
| PluginEntity[] memory preRuntimeValidationHooks = | ||
| _storage.validationData[directCallValidationKey].preValidationHooks; | ||
|
|
||
| uint256 hookLen = preRuntimeValidationHooks.length; | ||
| for (uint256 i = 0; i < hookLen; ++i) { | ||
| _doPreRuntimeValidationHook(preRuntimeValidationHooks[i], msg.data, ""); | ||
| } | ||
|
|
||
| // Permission hooks | ||
| PostExecToRun[] memory postPermissionHooks = | ||
| _doPreHooks(_storage.validationData[directCallValidationKey].permissionHooks, msg.data); | ||
|
|
||
| // Exec hooks | ||
| PostExecToRun[] memory postExecutionHooks = | ||
| _doPreHooks(_storage.selectorData[msg.sig].executionHooks, msg.data); | ||
|
|
||
| return (postPermissionHooks, postExecutionHooks); | ||
| } | ||
|
|
||
| function _checkIfValidationAppliesCallData( | ||
| bytes calldata callData, | ||
| PluginEntity validationFunction, | ||
|
|
@@ -661,25 +719,6 @@ contract UpgradeableModularAccount is | |
| } | ||
| } | ||
|
|
||
| function _checkIfValidationAppliesSelector(bytes4 selector, PluginEntity validationFunction, bool isGlobal) | ||
| internal | ||
| view | ||
| { | ||
| AccountStorage storage _storage = getAccountStorage(); | ||
|
|
||
| // Check that the provided validation function is applicable to the selector | ||
| if (isGlobal) { | ||
| if (!_globalValidationAllowed(selector) || !_storage.validationData[validationFunction].isGlobal) { | ||
| revert UserOpValidationFunctionMissing(selector); | ||
| } | ||
| } else { | ||
| // Not global validation, but per-selector | ||
| if (!getAccountStorage().validationData[validationFunction].selectors.contains(toSetValue(selector))) { | ||
| revert UserOpValidationFunctionMissing(selector); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function _globalValidationAllowed(bytes4 selector) internal view returns (bool) { | ||
| if ( | ||
| selector == this.execute.selector || selector == this.executeBatch.selector | ||
|
|
@@ -693,14 +732,22 @@ contract UpgradeableModularAccount is | |
| return getAccountStorage().selectorData[selector].allowGlobalValidation; | ||
| } | ||
|
|
||
| function _checkPermittedCallerIfNotFromEP() internal view { | ||
| function _checkIfValidationAppliesSelector(bytes4 selector, PluginEntity validationFunction, bool isGlobal) | ||
| internal | ||
| view | ||
| { | ||
| AccountStorage storage _storage = getAccountStorage(); | ||
|
|
||
| if ( | ||
| msg.sender != address(_ENTRY_POINT) && msg.sender != address(this) | ||
| && !_storage.selectorData[msg.sig].isPublic | ||
| ) { | ||
| revert ExecFromPluginNotPermitted(msg.sender, msg.sig); | ||
| // Check that the provided validation function is applicable to the selector | ||
| if (isGlobal) { | ||
| if (!_globalValidationAllowed(selector) || !_storage.validationData[validationFunction].isGlobal) { | ||
| revert ValidationFunctionMissing(selector); | ||
| } | ||
| } else { | ||
| // Not global validation, but per-selector | ||
| if (!getAccountStorage().validationData[validationFunction].selectors.contains(toSetValue(selector))) { | ||
| revert ValidationFunctionMissing(selector); | ||
| } | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.