diff --git a/examples/src/HookMultiplex/HookMultiplexer.sol b/examples/src/HookMultiplex/HookMultiplexer.sol index f2699f86..9c0fce70 100644 --- a/examples/src/HookMultiplex/HookMultiplexer.sol +++ b/examples/src/HookMultiplex/HookMultiplexer.sol @@ -3,38 +3,73 @@ pragma solidity ^0.8.23; import { Execution } from "@rhinestone/modulekit/src/Accounts.sol"; import { SENTINEL, SentinelListLib } from "sentinellist/SentinelList.sol"; +import { SENTINEL as SENTINELBytes32, LinkedBytes32Lib } from "sentinellist/SentinelListBytes32.sol"; import { ERC7579HookDestruct } from "@rhinestone/modulekit/src/modules/ERC7579HookDestruct.sol"; import "forge-std/console2.sol"; import { ISubHook } from "./ISubHook.sol"; +import { IHookMultiPlexer, hookFlag } from "./IHookMultiplexer.sol"; +import { HookEncodingLib } from "./lib/HookEncodingLib.sol"; -contract HookMultiPlexer is ERC7579HookDestruct { +bytes32 constant STORAGE_SLOT = bytes32(uint256(1_244_444_444)); + +contract HookMultiPlexer is ERC7579HookDestruct, IHookMultiPlexer { using SentinelListLib for SentinelListLib.SentinelList; + using LinkedBytes32Lib for LinkedBytes32Lib.LinkedBytes32; + using HookEncodingLib for ConfigParam; + using HookEncodingLib for bytes32; uint256 internal constant MAX_HOOK_NR = 16; - mapping(address smartAccount => SentinelListLib.SentinelList globalSubHooks) internal - $globalSubHooks; - mapping(address smartAccount => mapping(address module => SentinelListLib.SentinelList)) - internal $moduleSubHooks; - function installGlobalHooks(address[] memory hooks) public { - uint256 length = hooks.length; + error SubHookFailed(bytes32 hook); + + struct MultiPlexerStorage { + mapping(address smartAccount => LinkedBytes32Lib.LinkedBytes32 globalSubHooks) + $globalSubHooks; + mapping(address smartAccount => mapping(address module => LinkedBytes32Lib.LinkedBytes32)) + $moduleSubHooks; + } + + function $multiplexer() internal returns (MultiPlexerStorage storage strg) { + bytes32 position = STORAGE_SLOT; + assembly { + strg.slot := position + } + } + + function installGlobalHooks(ConfigParam[] calldata params) public { + uint256 length = params.length; for (uint256 i; i < length; i++) { - $globalSubHooks[msg.sender].push(hooks[i]); - // TODO check if the hook is already enabled for module + ConfigParam calldata conf = params[i]; + bytes32 _packed = conf.pack(); + console2.logBytes32(_packed); + console2.log( + conf.hook, + hookFlag.unwrap(conf.isValidatorHook), + hookFlag.unwrap(conf.isExecutorHook) + ); + $multiplexer().$globalSubHooks[msg.sender].push(_packed); } } - function installModuleHooks(address module, address[] memory hooks) public { - uint256 length = hooks.length; + function installModuleHooks(address module, ConfigParam[] calldata params) public { + uint256 length = params.length; + $multiplexer().$moduleSubHooks[msg.sender][module].init(); for (uint256 i; i < length; i++) { + ConfigParam calldata conf = params[i]; + bytes32 _packed = conf.pack(); // check if the hook is already enabled for global - if ($globalSubHooks[msg.sender].contains(hooks[i])) continue; - $moduleSubHooks[msg.sender][module].push(hooks[i]); + if ($multiplexer().$globalSubHooks[msg.sender].contains(_packed)) continue; + $multiplexer().$moduleSubHooks[msg.sender][module].push(_packed); } } + function configSubHook(address module, bytes32 hook, bytes calldata configCallData) external { + if (!$multiplexer().$moduleSubHooks[msg.sender][module].contains(hook)) revert(); + (bool success,) = hook.decodeAddress().call(configCallData); + } + function onInstall(bytes calldata data) external override { - $globalSubHooks[msg.sender].init(); + $multiplexer().$globalSubHooks[msg.sender].init(); } function onUninstall(bytes calldata data) external override { @@ -46,7 +81,7 @@ contract HookMultiPlexer is ERC7579HookDestruct { } function name() external pure virtual returns (string memory) { - return "PermissionHook"; + return "MultiPlexerHook"; } function isModuleType(uint256 isType) external pure virtual override returns (bool) { @@ -57,19 +92,46 @@ contract HookMultiPlexer is ERC7579HookDestruct { // todo } - function _delegatecallSubHook( - bytes4 functionSig, - address subHook, - address msgSender, - address target, - uint256 value, - bytes calldata callData + function _execSubHooks( + address module, + bytes memory callData, + function (bytes32) returns(bool) checkFlagFn ) internal - { } + { + (bytes32[] memory globalHooks,) = $multiplexer().$globalSubHooks[msg.sender] + .getEntriesPaginated(SENTINELBytes32, MAX_HOOK_NR); + + uint256 length = globalHooks.length; + + for (uint256 i; i < length; i++) { + bytes32 _globalHook = globalHooks[i]; + console2.logBytes32(_globalHook); + console2.log("flag", checkFlagFn(_globalHook)); + if (!checkFlagFn(_globalHook)) continue; + (bool success,) = _globalHook.decodeAddress().call(callData); + if (!success) revert SubHookFailed(_globalHook); + } + + LinkedBytes32Lib.LinkedBytes32 storage $moduleHooks = + $multiplexer().$moduleSubHooks[msg.sender][module]; + // TODO: make this nicer + if (!$moduleHooks.alreadyInitialized()) return; + + (bytes32[] memory moduleHooks,) = $multiplexer().$moduleSubHooks[msg.sender][module] + .getEntriesPaginated(SENTINELBytes32, MAX_HOOK_NR); + length = moduleHooks.length; + for (uint256 i; i < length; i++) { + bytes32 _moduleHook = moduleHooks[i]; + + if (!checkFlagFn(_moduleHook)) continue; + (bool success,) = _moduleHook.decodeAddress().call(callData); + if (!success) revert SubHookFailed(_moduleHook); + } + } function onExecute( - address msgSender, + address module, address target, uint256 value, bytes calldata callData @@ -79,37 +141,31 @@ contract HookMultiPlexer is ERC7579HookDestruct { override returns (bytes memory hookData) { - console2.log("onExecute: msgSender", msg.sender); - (address[] memory globalHooks,) = - $globalSubHooks[msg.sender].getEntriesPaginated(SENTINEL, MAX_HOOK_NR); - - uint256 length = globalHooks.length; - console2.log("globalHooks.length", length); - - for (uint256 i; i < length; i++) { - address hook = globalHooks[i]; - (bool success,) = hook.delegatecall( - abi.encodeCall(ISubHook.onExecute, (msgSender, target, value, callData)) - ); - require(success, "HookMultiPlexer: onExecute: subhook failed"); - } - - // TODO: - // implement for loop for module specific hooks + _execSubHooks({ + module: module, + callData: abi.encodeCall(ISubHook.onExecute, (msg.sender, module, target, value, callData)), + checkFlagFn: HookEncodingLib.is4337Hook + }); } function onExecuteBatch( - address msgSender, - Execution[] calldata + address module, + Execution[] calldata executions ) internal virtual override returns (bytes memory hookData) - { } + { + _execSubHooks({ + module: module, + callData: abi.encodeCall(ISubHook.onExecuteBatch, (msg.sender, module, executions)), + checkFlagFn: HookEncodingLib.is4337Hook + }); + } function onExecuteFromExecutor( - address msgSender, + address module, address target, uint256 value, bytes calldata callData @@ -118,41 +174,73 @@ contract HookMultiPlexer is ERC7579HookDestruct { virtual override returns (bytes memory hookData) - { } + { + _execSubHooks({ + module: module, + callData: abi.encodeCall( + ISubHook.onExecuteFromExecutor, (msg.sender, module, target, value, callData) + ), + checkFlagFn: HookEncodingLib.isExecutorHook + }); + } function onExecuteBatchFromExecutor( - address msgSender, - Execution[] calldata + address module, + Execution[] calldata executions ) internal virtual override returns (bytes memory hookData) - { } + { + _execSubHooks({ + module: module, + callData: abi.encodeCall( + ISubHook.onExecuteBatchFromExecutor, (msg.sender, module, executions) + ), + checkFlagFn: HookEncodingLib.isExecutorHook + }); + } function onInstallModule( - address msgSender, - uint256 moduleType, address module, + uint256 moduleType, + address installModule, bytes calldata initData ) internal virtual override returns (bytes memory hookData) - { } + { + _execSubHooks({ + module: module, + callData: abi.encodeCall( + ISubHook.onInstallModule, (msg.sender, module, moduleType, installModule, initData) + ), + checkFlagFn: HookEncodingLib.isConfigHook + }); + } function onUninstallModule( - address msgSender, - uint256 moduleType, address module, + uint256 moduleType, + address uninstallModule, bytes calldata deInitData ) internal virtual override returns (bytes memory hookData) - { } + { + _execSubHooks({ + module: module, + callData: abi.encodeCall( + ISubHook.onInstallModule, (msg.sender, module, moduleType, uninstallModule, deInitData) + ), + checkFlagFn: HookEncodingLib.isConfigHook + }); + } function onPostCheck(bytes calldata hookData) internal diff --git a/examples/src/HookMultiplex/IHookMultiplexer.sol b/examples/src/HookMultiplex/IHookMultiplexer.sol new file mode 100644 index 00000000..a883ddac --- /dev/null +++ b/examples/src/HookMultiplex/IHookMultiplexer.sol @@ -0,0 +1,10 @@ +type hookFlag is bool; + +interface IHookMultiPlexer { + struct ConfigParam { + address hook; + hookFlag isExecutorHook; + hookFlag isValidatorHook; + hookFlag isConfigHook; + } +} diff --git a/examples/src/HookMultiplex/ISubHook.sol b/examples/src/HookMultiplex/ISubHook.sol index ee2810b9..1852bf04 100644 --- a/examples/src/HookMultiplex/ISubHook.sol +++ b/examples/src/HookMultiplex/ISubHook.sol @@ -5,7 +5,8 @@ import { Execution } from "@rhinestone/modulekit/src/Accounts.sol"; interface ISubHook { function onExecute( - address msgSender, + address smartAccount, + address module, address target, uint256 value, bytes calldata callData @@ -14,14 +15,16 @@ interface ISubHook { returns (bytes memory hookData); function onExecuteBatch( - address msgSender, - Execution[] calldata + address smartAccount, + address module, + Execution[] calldata executions ) external returns (bytes memory hookData); function onExecuteFromExecutor( - address msgSender, + address smartAccount, + address module, address target, uint256 value, bytes calldata callData @@ -30,8 +33,9 @@ interface ISubHook { returns (bytes memory hookData); function onExecuteBatchFromExecutor( - address msgSender, - Execution[] calldata + address smartAccount, + address module, + Execution[] calldata executions ) external returns (bytes memory hookData); @@ -41,18 +45,20 @@ interface ISubHook { //////////////////////////////////////////////////////////////////////////*/ function onInstallModule( - address msgSender, - uint256 moduleType, + address smartAccount, address module, + uint256 moduleType, + address moduleToInstall, bytes calldata initData ) external returns (bytes memory hookData); function onUninstallModule( - address msgSender, - uint256 moduleType, + address smartAccount, address module, + uint256 moduleType, + address moduleToUninstall, bytes calldata deInitData ) external diff --git a/examples/src/HookMultiplex/lib/HookEncodingLib.sol b/examples/src/HookMultiplex/lib/HookEncodingLib.sol new file mode 100644 index 00000000..ad2f7262 --- /dev/null +++ b/examples/src/HookMultiplex/lib/HookEncodingLib.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "../IHookMultiplexer.sol"; +import "forge-std/console2.sol"; + +library HookEncodingLib { + function pack(IHookMultiPlexer.ConfigParam calldata params) + internal + pure + returns (bytes32 encoded) + { + return + encode(params.hook, params.isExecutorHook, params.isValidatorHook, params.isConfigHook); + } + + function encode( + address hook, + hookFlag isExecutorHook, + hookFlag isValidatorHook, + hookFlag isConfigHook + ) + internal + pure + returns (bytes32 encoded) + { + assembly { + encoded := hook + encoded := or(encoded, shl(8, isExecutorHook)) + encoded := or(encoded, shl(16, isValidatorHook)) + encoded := or(encoded, shl(24, isConfigHook)) + } + encoded = bytes32( + (abi.encodePacked(isExecutorHook, isValidatorHook, isConfigHook, bytes5(0), hook)) + ); + } + + function decode(bytes32 encoded) + internal + pure + returns ( + address hook, + hookFlag isExecutorHook, + hookFlag isValidatorHook, + hookFlag isConfigHook + ) + { + assembly { + hook := encoded + isExecutorHook := shr(8, encoded) + isValidatorHook := shr(16, encoded) + isConfigHook := shr(24, encoded) + } + } + + function isExecutorHook(bytes32 encoded) internal pure returns (bool) { + return (uint256(encoded)) & 0xff == 1; + } + + function is4337Hook(bytes32 encoded) internal pure returns (bool) { + console2.log("----"); + console2.logBytes32(encoded); + + bytes32 foo; + + assembly { + + foo := shr(encoded, 8) + + } + console2.logBytes32(foo); + console2.log("----"); + + return (uint256(encoded) >> 8) & 0xff == 1; + } + + function isConfigHook(bytes32 encoded) internal pure returns (bool) { + return (uint256(encoded) >> 16) & 0xff == 1; + } + + function decodeAddress(bytes32 encoded) internal pure returns (address) { + return address(uint160(uint256(encoded))); + } +} diff --git a/examples/src/HookMultiplex/subHooks/PermissionFlags.sol b/examples/src/HookMultiplex/subHooks/PermissionFlags.sol index 14eef649..30320477 100644 --- a/examples/src/HookMultiplex/subHooks/PermissionFlags.sol +++ b/examples/src/HookMultiplex/subHooks/PermissionFlags.sol @@ -4,14 +4,14 @@ pragma solidity ^0.8.23; import { SentinelListLib } from "sentinellist/SentinelList.sol"; import { LinkedBytes32Lib } from "sentinellist/SentinelListBytes32.sol"; import { Execution } from "@rhinestone/modulekit/src/Accounts.sol"; -import { ISubHook } from "../ISubHook.sol"; +import { SubHookBase } from "./SubHookBase.sol"; import { TokenTransactionLib } from "../lib/TokenTransactionLib.sol"; import "forge-std/console2.sol"; // bytes32 constant STORAGE_SLOT = keccak256("permissions.storage"); bytes32 constant STORAGE_SLOT = bytes32(uint256(123)); -contract PermissionFlags is ISubHook { +contract PermissionFlags is SubHookBase { using SentinelListLib for SentinelListLib.SentinelList; using LinkedBytes32Lib for LinkedBytes32Lib.LinkedBytes32; using TokenTransactionLib for bytes4; @@ -44,6 +44,8 @@ contract PermissionFlags is ISubHook { mapping(address account => mapping(address module => ModulePermissions)) permissions; } + constructor(address HookMultiplexer) SubHookBase(HookMultiplexer) { } + function $subHook() internal pure virtual returns (SubHookStorage storage shs) { bytes32 position = STORAGE_SLOT; assembly { @@ -74,6 +76,7 @@ contract PermissionFlags is ISubHook { } function onExecute( + address smartAccount, address superVisorModule, address target, uint256 value, @@ -81,19 +84,19 @@ contract PermissionFlags is ISubHook { ) external virtual - override + onlyMultiplexer returns (bytes memory hookData) { console2.log("onExecute subhook"); ModulePermissions storage $modulePermissions = - $subHook().permissions[msg.sender][superVisorModule]; + $subHook().permissions[smartAccount][superVisorModule]; AccessFlags memory flags = $modulePermissions.flags; bytes4 functionSig = callData.length > 4 ? bytes4(callData[0:4]) : bytes4(0); // check for self call - if (!flags.selfCall && target == msg.sender) { + if (!flags.selfCall && target == smartAccount) { revert InvalidPermission(); } @@ -133,6 +136,7 @@ contract PermissionFlags is ISubHook { } function onExecuteBatch( + address smartAccount, address superVisorModule, Execution[] calldata ) @@ -143,6 +147,7 @@ contract PermissionFlags is ISubHook { { } function onExecuteFromExecutor( + address smartAccount, address superVisorModule, address target, uint256 value, @@ -155,6 +160,7 @@ contract PermissionFlags is ISubHook { { } function onExecuteBatchFromExecutor( + address smartAccount, address superVisorModule, Execution[] calldata ) @@ -165,6 +171,7 @@ contract PermissionFlags is ISubHook { { } function onInstallModule( + address smartAccount, address superVisorModule, uint256 moduleType, address module, @@ -177,6 +184,7 @@ contract PermissionFlags is ISubHook { { } function onUninstallModule( + address smartAccount, address superVisorModule, uint256 moduleType, address module, @@ -188,10 +196,5 @@ contract PermissionFlags is ISubHook { returns (bytes memory hookData) { } - function onPostCheck(bytes calldata hookData) - external - virtual - override - returns (bool success) - { } + function onPostCheck(bytes calldata hookData) external virtual returns (bool success) { } } diff --git a/examples/src/HookMultiplex/subHooks/SpendingLimits.sol b/examples/src/HookMultiplex/subHooks/SpendingLimits.sol index 96ba00cd..0c4992ca 100644 --- a/examples/src/HookMultiplex/subHooks/SpendingLimits.sol +++ b/examples/src/HookMultiplex/subHooks/SpendingLimits.sol @@ -4,14 +4,14 @@ pragma solidity ^0.8.23; import { SentinelListLib } from "sentinellist/SentinelList.sol"; import { LinkedBytes32Lib } from "sentinellist/SentinelListBytes32.sol"; import { Execution } from "@rhinestone/modulekit/src/Accounts.sol"; -import { ISubHook } from "../ISubHook.sol"; +import { SubHookBase } from "./SubHookBase.sol"; import { TokenTransactionLib } from "../lib/TokenTransactionLib.sol"; import "forge-std/console2.sol"; // bytes32 constant STORAGE_SLOT = keccak256("permissions.storage"); bytes32 constant STORAGE_SLOT = bytes32(uint256(123_123_123_123)); -contract SpendingLimits is ISubHook { +contract SpendingLimits is SubHookBase { using TokenTransactionLib for bytes4; struct Limits { @@ -23,6 +23,8 @@ contract SpendingLimits is ISubHook { mapping(address smartAccount => mapping(address token => Limits limit)) limit; } + constructor(address HookMultiplexer) SubHookBase(HookMultiplexer) { } + function $subHook() internal pure virtual returns (SubHookStorage storage shs) { bytes32 position = STORAGE_SLOT; assembly { @@ -36,13 +38,15 @@ contract SpendingLimits is ISubHook { } function onExecute( - address msgSender, + address smartAccount, + address module, address target, uint256 value, bytes calldata callData ) external override + onlyMultiplexer returns (bytes memory hookData) { if (callData.length < 4) return ""; @@ -50,7 +54,7 @@ contract SpendingLimits is ISubHook { (address to, uint256 amount) = abi.decode(callData[4:], (address, uint256)); - Limits storage $limit = $subHook().limit[msg.sender][target]; + Limits storage $limit = $subHook().limit[smartAccount][target]; uint256 totalSpent = $limit.totalSpent + amount; console2.log("totalSpent", totalSpent); require(totalSpent <= $limit.limit, "SpendingLimit: limit exceeded"); @@ -58,55 +62,63 @@ contract SpendingLimits is ISubHook { } function onExecuteBatch( - address msgSender, + address smartAccount, + address module, Execution[] calldata ) external override + onlyMultiplexer returns (bytes memory hookData) { } function onExecuteFromExecutor( - address msgSender, + address smartAccount, + address module, address target, uint256 value, bytes calldata callData ) external override + onlyMultiplexer returns (bytes memory hookData) { } function onExecuteBatchFromExecutor( - address msgSender, - Execution[] calldata + address smartAccount, + address module, + Execution[] calldata executions ) external override + onlyMultiplexer returns (bytes memory hookData) { } function onInstallModule( - address msgSender, - uint256 moduleType, + address smartAccount, address module, + uint256 moduleType, + address moduleToInstall, bytes calldata initData ) external - override + onlyMultiplexer returns (bytes memory hookData) { } function onUninstallModule( - address msgSender, - uint256 moduleType, + address smartAccount, address module, + uint256 moduleType, + address moduleToUninstall, bytes calldata deInitData ) external - override + onlyMultiplexer returns (bytes memory hookData) { } - function onPostCheck(bytes calldata hookData) external override returns (bool success) { } + function onPostCheck(bytes calldata hookData) external returns (bool success) { } } diff --git a/examples/src/HookMultiplex/subHooks/SubHookBase.sol b/examples/src/HookMultiplex/subHooks/SubHookBase.sol new file mode 100644 index 00000000..c510cb12 --- /dev/null +++ b/examples/src/HookMultiplex/subHooks/SubHookBase.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { ISubHook } from "../ISubHook.sol"; + +abstract contract SubHookBase is ISubHook { + address internal immutable MULTIPLEXER; + + error Unauthorized(); + + constructor(address multiplexer) { + MULTIPLEXER = multiplexer; + } + + modifier onlyMultiplexer() { + if (msg.sender != MULTIPLEXER) revert Unauthorized(); + _; + } +} diff --git a/examples/test/HookMultiPlexer/HookMultiplexer.t.sol b/examples/test/HookMultiPlexer/HookMultiplexer.t.sol index e26ea0b9..7be8716e 100644 --- a/examples/test/HookMultiPlexer/HookMultiplexer.t.sol +++ b/examples/test/HookMultiPlexer/HookMultiplexer.t.sol @@ -14,7 +14,7 @@ import { Solarray } from "solarray/Solarray.sol"; import { MODULE_TYPE_HOOK, MODULE_TYPE_EXECUTOR } from "@rhinestone/modulekit/src/external/ERC7579.sol"; -import { HookMultiPlexer } from "src/HookMultiPlex/HookMultiPlexer.sol"; +import { IHookMultiPlexer, HookMultiPlexer, hookFlag } from "src/HookMultiPlex/HookMultiPlexer.sol"; import { PermissionFlags } from "src/HookMultiPlex/subHooks/PermissionFlags.sol"; import { SpendingLimits } from "src/HookMultiPlex/subHooks/SpendingLimits.sol"; @@ -43,9 +43,9 @@ contract HookMultiPlexerTest is RhinestoneModuleKit, Test { multiplexer = new HookMultiPlexer(); vm.label(address(multiplexer), "multiplexer"); - permissionFlagsSubHook = new PermissionFlags(); + permissionFlagsSubHook = new PermissionFlags(address(multiplexer)); vm.label(address(permissionFlagsSubHook), "SubHook:PermissionFlags"); - spendingLimitsSubHook = new SpendingLimits(); + spendingLimitsSubHook = new SpendingLimits(address(multiplexer)); vm.label(address(spendingLimitsSubHook), "SubHook:SpendingLimits"); executorDisallowed = new MockExecutor(); @@ -73,10 +73,24 @@ contract HookMultiPlexerTest is RhinestoneModuleKit, Test { }); vm.prank(instance.account); - address[] memory globalHooks = new address[](2); - globalHooks[0] = address(permissionFlagsSubHook); - globalHooks[1] = address(spendingLimitsSubHook); - multiplexer.installGlobalHooks(globalHooks); + + IHookMultiPlexer.ConfigParam[] memory globalHooksConfig = + new IHookMultiPlexer.ConfigParam[](2); + globalHooksConfig[0] = IHookMultiPlexer.ConfigParam({ + hook: address(permissionFlagsSubHook), + isExecutorHook: hookFlag.wrap(false), + isValidatorHook: hookFlag.wrap(true), + isConfigHook: hookFlag.wrap(false) + }); + + globalHooksConfig[1] = IHookMultiPlexer.ConfigParam({ + hook: address(spendingLimitsSubHook), + isExecutorHook: hookFlag.wrap(false), + isValidatorHook: hookFlag.wrap(true), + isConfigHook: hookFlag.wrap(false) + }); + + multiplexer.installGlobalHooks(globalHooksConfig); } function setUpPermissionsSubHook() internal {