Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fe5af61
feat: initial impl of simple plugin direct calls with validation hooks
Zer0dot Jul 10, 2024
b11b7b4
chore: remove unused import, formatting
Zer0dot Jul 10, 2024
222243e
chore: slight refactor for codesize
Zer0dot Jul 11, 2024
e1adaca
feat: add direct call selectors to plugin manifest and installation
Zer0dot Jul 11, 2024
1d45b2e
feat: test basic direct plugin call functionality
Zer0dot Jul 11, 2024
883eff5
test: extra scenario test, minor renaming
Zer0dot Jul 11, 2024
691dd31
refactor: migrate direct call installation to installValidation
Zer0dot Jul 12, 2024
4a99058
chore: cleanup comments
Zer0dot Jul 12, 2024
efc111f
chore: slight cleanup and renaming
Zer0dot Jul 13, 2024
2a58118
test: add permission hooks to direct plugin call tests
Zer0dot Jul 15, 2024
d614b0e
chore: update permission hook uninstallation to handle full execution…
Zer0dot Jul 15, 2024
f0b8321
refactor: refactor while to for loop for permission hook uninstallation
Zer0dot Jul 16, 2024
43a3e5c
chore: document direct-call flow
Zer0dot Jul 16, 2024
e615bc2
refactor: consolidate pre-runtime-hooks into internal function
Zer0dot Jul 16, 2024
b537f07
Merge branch 'v0.8-develop' into zer0dot/direct-plugin-calls
Zer0dot Jul 16, 2024
b3b834f
chore: remove unused import
Zer0dot Jul 16, 2024
4aa008c
chore: remove unused imports
Zer0dot Jul 16, 2024
ae26e5f
chore: linting changes
Zer0dot Jul 16, 2024
a09ffb2
chore: remove unused struct and using for statements
Zer0dot Jul 16, 2024
a978cd7
feat: use _checkIfValidationAppliesCallData() rather than manually ch…
Zer0dot Jul 16, 2024
ad4fcb6
chore: rename missing validation error to encapsulate runtime as well
Zer0dot Jul 17, 2024
d263656
chore: fix function ordering
Zer0dot Jul 17, 2024
0f04d85
chore: double linter max line-length for test error strings
Zer0dot Jul 17, 2024
32c39fb
Merge branch 'v0.8-develop' into zer0dot/direct-plugin-calls
Zer0dot Jul 17, 2024
a425c9b
chore: formatting
Zer0dot Jul 17, 2024
4a29ae3
chore: rename old function to modern naming (functionReference => plu…
Zer0dot Jul 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions .solhint-test.json
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 }]
}
}
2 changes: 1 addition & 1 deletion src/account/AccountStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ struct ValidationData {
bool isGlobal;
// Whether or not this validation is a signature validator.
bool isSignatureValidation;
// The pre validation hooks for this function selector.
// The pre validation hooks for this validation function.
PluginEntity[] preValidationHooks;
// Permission hooks for this validation function.
EnumerableSet.Bytes32Set permissionHooks;
Expand Down
35 changes: 19 additions & 16 deletions src/account/PluginManager2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
Expand All @@ -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) =
Expand Down Expand Up @@ -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) {
Expand All @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
}
}
}

Expand Down Expand Up @@ -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;
Expand Down
131 changes: 89 additions & 42 deletions src/account/UpgradeableModularAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -136,7 +135,7 @@ contract UpgradeableModularAccount is
revert UnrecognizedFunction(msg.sig);
}

_checkPermittedCallerIfNotFromEP();
_checkPermittedCallerAndAssociatedHooks();
Copy link
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I.e. if a signer, as set in the storage of SingleSignerValidation, tries to call the account? In that case, this check would fail, the signer would need to use executeWithAuthorization to trigger runtime validation.

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.

Copy link

Choose a reason for hiding this comment

The 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 _checkPermittedCallerAndAssociatedHooks() in L664 we do the pre-execution hooks of the function selector, but again in the following fallback() flow in L143.
Also, are we not forgetting to do the post-permission hooks in the fallback flow? 👀

Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
}
}
}
2 changes: 1 addition & 1 deletion src/helpers/ValidationConfigLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ library ValidationConfigLib {
return uint32(bytes4(ValidationConfig.unwrap(config) << 160));
}

function functionReference(ValidationConfig config) internal pure returns (PluginEntity) {
function pluginEntity(ValidationConfig config) internal pure returns (PluginEntity) {
return PluginEntity.wrap(bytes24(ValidationConfig.unwrap(config)));
}

Expand Down
Loading