From dfa0e0a540fb0699ce13686e7b5935571578df82 Mon Sep 17 00:00:00 2001 From: kopy-kat Date: Sat, 24 Feb 2024 13:36:56 +0000 Subject: [PATCH 1/9] feat: init permissions hook --- .../src/PermissionsHook/PermissionsHook.sol | 294 ++++++++++++++++ .../PermissionsHook/PermissionsHook.t.sol | 313 ++++++++++++++++++ 2 files changed, 607 insertions(+) create mode 100644 examples/src/PermissionsHook/PermissionsHook.sol create mode 100644 examples/test/PermissionsHook/PermissionsHook.t.sol diff --git a/examples/src/PermissionsHook/PermissionsHook.sol b/examples/src/PermissionsHook/PermissionsHook.sol new file mode 100644 index 00000000..1573f348 --- /dev/null +++ b/examples/src/PermissionsHook/PermissionsHook.sol @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { ERC7579HookDestruct } from "@rhinestone/modulekit/src/modules/ERC7579HookDestruct.sol"; +import { Execution, IERC7579Account } from "@rhinestone/modulekit/src/Accounts.sol"; + +import { IERC721 } from "forge-std/interfaces/IERC721.sol"; +import { IERC20 } from "forge-std/interfaces/IERC20.sol"; + +contract PermissionsHook is ERC7579HookDestruct { + /*////////////////////////////////////////////////////////////////////////// + CONSTANTS + //////////////////////////////////////////////////////////////////////////*/ + + bytes1 internal constant PERMISSION_DISALLOWED = 0x00; + bytes1 internal constant PERMISSION_ALLOWED = 0x00; + + error InvalidPermission(); + + struct ModulePermissions { + // Execution permissions + // - Target permissions + bytes1 selfCall; // 0x00 - false, 0x01 - true + bytes1 moduleCall; // 0x00 - false, 0x01 - true + // - Value permissions + bytes1 sendValue; // 0x00 - false, 0x01 - true + // - Calldata permissions + bytes1 erc20Transfer; // 0x00 - false, 0x01 - true + bytes1 erc721Transfer; // 0x00 - false, 0x01 - true + // Module configuration permissions + bytes1 moduleConfig; // 0x00 - false, 0x01 - true + } + + mapping(address account => mapping(address module => ModulePermissions)) internal permissions; + + /*////////////////////////////////////////////////////////////////////////// + CONFIG + //////////////////////////////////////////////////////////////////////////*/ + + function onInstall(bytes calldata data) external override { + (address[] memory _modules, ModulePermissions[] memory _permissions) = + abi.decode(data, (address[], ModulePermissions[])); + + uint256 permissionsLength = _permissions.length; + + if (_modules.length != permissionsLength) { + revert("PermissionsHook: addPermissions: module and permissions length mismatch"); + } + + for (uint256 i = 0; i < permissionsLength; i++) { + permissions[msg.sender][_modules[i]] = _permissions[i]; + } + } + + function onUninstall(bytes calldata data) external override { + // todo + } + + function isInitialized(address smartAccount) external view returns (bool) { + // todo + } + + function addPermissions( + address[] calldata _modules, + ModulePermissions[] calldata _permissions + ) + external + { + uint256 permissionsLength = _permissions.length; + + if (_modules.length != permissionsLength) { + revert("PermissionsHook: addPermissions: module and permissions length mismatch"); + } + + for (uint256 i = 0; i < permissionsLength; i++) { + permissions[msg.sender][_modules[i]] = _permissions[i]; + } + } + + function getPermissions( + address account, + address module + ) + public + view + returns (ModulePermissions memory) + { + return permissions[account][module]; + } + + /*////////////////////////////////////////////////////////////////////////// + MODULE LOGIC + //////////////////////////////////////////////////////////////////////////*/ + + function onPostCheck(bytes calldata hookData) + internal + virtual + override + returns (bool success) + { + return true; + } + + function onExecute( + address msgSender, + address target, + uint256 value, + bytes calldata callData + ) + internal + virtual + override + returns (bytes memory hookData) + { + // Not callable from module + return ""; + } + + function onExecuteBatch( + address msgSender, + Execution[] calldata + ) + internal + virtual + override + returns (bytes memory hookData) + { + // Not callable from module + return ""; + } + + function onExecuteFromExecutor( + address msgSender, + address target, + uint256 value, + bytes calldata callData + ) + internal + virtual + override + returns (bytes memory hookData) + { + ModulePermissions memory modulePermissions = permissions[msg.sender][msgSender]; + _validateExecutePermissions(modulePermissions, target, value, callData); + } + + function onExecuteBatchFromExecutor( + address msgSender, + Execution[] calldata executions + ) + internal + virtual + override + returns (bytes memory hookData) + { + ModulePermissions memory modulePermissions = permissions[msg.sender][msgSender]; + + uint256 executionLength = executions.length; + for (uint256 i = 0; i < executionLength; i++) { + _validateExecutePermissions( + modulePermissions, executions[i].target, executions[i].value, executions[i].callData + ); + } + } + + function onInstallModule( + address msgSender, + uint256 moduleType, + address module, + bytes calldata initData + ) + internal + virtual + override + returns (bytes memory hookData) + { + bool isInstalledExecutor = + IERC7579Account(msg.sender).isModuleInstalled(TYPE_EXECUTOR, msgSender, ""); + + if (!isInstalledExecutor) { + // Execution not triggered by executor, so account should do access control + return ""; + } + + ModulePermissions storage modulePermissions = permissions[msg.sender][msgSender]; + + if (modulePermissions.moduleConfig != PERMISSION_ALLOWED) { + revert InvalidPermission(); + } + } + + function onUninstallModule( + address msgSender, + uint256 moduleType, + address module, + bytes calldata deInitData + ) + internal + virtual + override + returns (bytes memory hookData) + { + bool isInstalledExecutor = + IERC7579Account(msg.sender).isModuleInstalled(TYPE_EXECUTOR, msgSender, ""); + + if (!isInstalledExecutor) { + // Execution not triggered by executor, so account should do access control + return ""; + } + + ModulePermissions storage modulePermissions = permissions[msg.sender][msgSender]; + + if (modulePermissions.moduleConfig != PERMISSION_ALLOWED) { + revert InvalidPermission(); + } + } + + /*////////////////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////////////////*/ + + function _validateExecutePermissions( + ModulePermissions memory modulePermissions, + address target, + uint256 value, + bytes calldata callData + ) + internal + { + // Target permissions + if (target == msg.sender && modulePermissions.selfCall != PERMISSION_ALLOWED) { + revert InvalidPermission(); + } + + if (modulePermissions.moduleCall != PERMISSION_ALLOWED) { + if (IERC7579Account(msg.sender).isModuleInstalled(TYPE_EXECUTOR, target, "")) { + revert InvalidPermission(); + } + } + + // Value permissions + if (value > 0 && modulePermissions.sendValue != PERMISSION_ALLOWED) { + revert InvalidPermission(); + } + + // Calldata permissions + if (_isErc20Transfer(callData) && modulePermissions.erc20Transfer != PERMISSION_ALLOWED) { + revert InvalidPermission(); + } + + if (_isErc721Transfer(callData) && modulePermissions.erc721Transfer != PERMISSION_ALLOWED) { + revert InvalidPermission(); + } + } + + function _isErc20Transfer(bytes calldata callData) + internal + pure + returns (bool isErc20Transfer) + { + bytes4 functionSig = bytes4(callData[0:4]); + if (functionSig == IERC20.transfer.selector || functionSig == IERC20.transferFrom.selector) + { + isErc20Transfer = true; + } + } + + function _isErc721Transfer(bytes calldata callData) + internal + pure + returns (bool isErc721Transfer) + { + bytes4 functionSig = bytes4(callData[0:4]); + if (functionSig == IERC721.transferFrom.selector) { + isErc721Transfer = true; + } + } + + /*////////////////////////////////////////////////////////////////////////// + METADATA + //////////////////////////////////////////////////////////////////////////*/ + + function version() external pure virtual returns (string memory) { + return "1.0.0"; + } + + function name() external pure virtual returns (string memory) { + return "ColdStorageHook"; + } + + function isModuleType(uint256 isType) external pure virtual override returns (bool) { + return isType == TYPE_HOOK; + } +} diff --git a/examples/test/PermissionsHook/PermissionsHook.t.sol b/examples/test/PermissionsHook/PermissionsHook.t.sol new file mode 100644 index 00000000..457daa7f --- /dev/null +++ b/examples/test/PermissionsHook/PermissionsHook.t.sol @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "forge-std/Test.sol"; +import "@rhinestone/modulekit/src/ModuleKit.sol"; +import "@rhinestone/modulekit/src/Modules.sol"; +import "@rhinestone/modulekit/src/Helpers.sol"; +import "@rhinestone/modulekit/src/core/ExtensibleFallbackHandler.sol"; +import "@rhinestone/sessionkeymanager/src/ISessionValidationModule.sol"; +import { + SessionData, + SessionKeyManagerLib +} from "@rhinestone/sessionkeymanager/src/SessionKeyManagerLib.sol"; +import "@rhinestone/modulekit/src/Mocks.sol"; +import { Solarray } from "solarray/Solarray.sol"; +import { ECDSA } from "solady/src/utils/ECDSA.sol"; + +import { IERC7579Account, Execution } from "@rhinestone/modulekit/src/Accounts.sol"; +import { FlashloanCallback } from "src/ColdStorage/FlashloanCallback.sol"; +import { FlashloanLender } from "src/ColdStorage/FlashloanLender.sol"; +import { ColdStorageHook } from "src/ColdStorage/ColdStorageHook.sol"; +import { ColdStorageExecutor } from "src/ColdStorage/ColdStorageExecutor.sol"; +import { OwnableValidator } from "src/OwnableValidator/OwnableValidator.sol"; + +import { ERC7579BootstrapConfig } from "@rhinestone/modulekit/src/external/ERC7579.sol"; + +import "src/ColdStorage/interfaces/Flashloan.sol"; +import "erc7579/lib/ExecutionLib.sol"; + +contract ColdStorageTest is RhinestoneModuleKit, Test { + using ModuleKitHelpers for *; + using ModuleKitUserOp for *; + using ECDSA for bytes32; + + MockERC20 internal token; + + // main account and dependencies + AccountInstance internal mainAccount; + FlashloanCallback internal flashloanCallback; + + // ColdStorage Account and dependencies + AccountInstance internal coldStorage; + FlashloanLender internal flashloanLender; + ColdStorageHook internal coldStorageHook; + ColdStorageExecutor internal coldStorageExecutor; + OwnableValidator internal ownableValidator; + + MockValidator internal mockValidator; + + Account owner; + + function setUp() public { + init(); + + flashloanLender = new FlashloanLender(address(coldStorage.aux.fallbackHandler)); + vm.label(address(flashloanLender), "flashloanLender"); + flashloanCallback = new FlashloanCallback(address(mainAccount.aux.fallbackHandler)); + vm.label(address(flashloanCallback), "flashloanCallback"); + ownableValidator = new OwnableValidator(); + vm.label(address(ownableValidator), "ownableValidator"); + mockValidator = new MockValidator(); + vm.label(address(mockValidator), "mockValidator"); + + coldStorageHook = new ColdStorageHook(); + vm.label(address(coldStorageHook), "coldStorageHook"); + + coldStorageExecutor = new ColdStorageExecutor(); + vm.label(address(coldStorageExecutor), "coldStorageExecutor"); + + owner = makeAccount("owner"); + _setupMainAccount(); + _setUpColdstorage(); + + deal(address(coldStorage.account), 100 ether); + deal(address(mainAccount.account), 100 ether); + + token = new MockERC20(); + token.initialize("Mock Token", "MTK", 18); + deal(address(token), mainAccount.account, 100 ether); + + vm.warp(1_799_999); + + mainAccount.exec({ + target: address(token), + value: 0, + callData: abi.encodeCall(IERC20.transfer, (address(coldStorage.account), 1 ether)) + }); + } + + function _setupMainAccount() public { + ExtensibleFallbackHandler.Params[] memory params = new ExtensibleFallbackHandler.Params[](1); + params[0] = ExtensibleFallbackHandler.Params({ + selector: IERC3156FlashBorrower.onFlashLoan.selector, + fallbackType: ExtensibleFallbackHandler.FallBackType.Dynamic, + handler: address(flashloanCallback) + }); + + ERC7579BootstrapConfig[] memory validators = + makeBootstrapConfig(address(ownableValidator), abi.encode(owner.addr)); + ERC7579BootstrapConfig[] memory executors = + makeBootstrapConfig(address(flashloanCallback), abi.encode("")); + ERC7579BootstrapConfig memory hook = _emptyConfig(); + ERC7579BootstrapConfig memory fallBack = + _makeBootstrapConfig(address(auxiliary.fallbackHandler), abi.encode(params)); + mainAccount = makeAccountInstance("mainAccount", validators, executors, hook, fallBack); + } + + function _setUpColdstorage() public { + ExtensibleFallbackHandler.Params[] memory params = new ExtensibleFallbackHandler.Params[](6); + params[0] = ExtensibleFallbackHandler.Params({ + selector: IERC3156FlashLender.maxFlashLoan.selector, + fallbackType: ExtensibleFallbackHandler.FallBackType.Static, + handler: address(flashloanLender) + }); + params[1] = ExtensibleFallbackHandler.Params({ + selector: IERC3156FlashLender.flashFee.selector, + fallbackType: ExtensibleFallbackHandler.FallBackType.Static, + handler: address(flashloanLender) + }); + params[2] = ExtensibleFallbackHandler.Params({ + selector: IERC3156FlashLender.flashLoan.selector, + fallbackType: ExtensibleFallbackHandler.FallBackType.Dynamic, + handler: address(flashloanLender) + }); + params[3] = ExtensibleFallbackHandler.Params({ + selector: IERC6682.flashFeeToken.selector, + fallbackType: ExtensibleFallbackHandler.FallBackType.Static, + handler: address(flashloanLender) + }); + params[4] = ExtensibleFallbackHandler.Params({ + selector: IERC6682.flashFee.selector, + fallbackType: ExtensibleFallbackHandler.FallBackType.Static, + handler: address(flashloanLender) + }); + params[5] = ExtensibleFallbackHandler.Params({ + selector: IERC6682.availableForFlashLoan.selector, + fallbackType: ExtensibleFallbackHandler.FallBackType.Static, + handler: address(flashloanLender) + }); + + ERC7579BootstrapConfig[] memory validators = + makeBootstrapConfig(address(ownableValidator), abi.encode(address(mainAccount.account))); + + address[] memory addresses = new address[](2); + bytes[] memory callData = new bytes[](2); + + addresses[0] = address(flashloanLender); + addresses[1] = address(coldStorageExecutor); + + callData[0] = abi.encode(""); + callData[1] = abi.encodePacked(address(mainAccount.account)); + + ERC7579BootstrapConfig[] memory executors = makeBootstrapConfig(addresses, callData); + + ERC7579BootstrapConfig memory hook = _makeBootstrapConfig( + address(coldStorageHook), abi.encode(uint128(7 days), address(mainAccount.account)) + ); + ERC7579BootstrapConfig memory fallBack = + _makeBootstrapConfig(address(auxiliary.fallbackHandler), abi.encode(params)); + + coldStorage = makeAccountInstance("coldStorage", validators, executors, hook, fallBack); + } + + function simulateDeposit() internal { + vm.prank(mainAccount.account); + token.transfer(coldStorage.account, 1 ether); + } + + function signHash(uint256 privKey, bytes32 digest) internal returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privKey, ECDSA.toEthSignedMessageHash(digest)); + return abi.encodePacked(r, s, v); + } + + function _deploySubAccount() private { + // todo: replace with modulekit function to deploy account + // create and exec an empty user op to deploy the sub account + UserOpData memory userOpData = + coldStorage.getExecOps(address(0), 0, "", address(ownableValidator)); + + bytes memory signature = signHash(owner.key, userOpData.userOpHash); + signature = abi.encodePacked(address(ownableValidator), signature); + userOpData.userOp.signature = signature; + coldStorage.expect4337Revert(); + userOpData.execUserOps(); + } + + function _requestWithdraw(Execution memory exec, uint256 additionalDelay) internal { + bytes memory subAccountCallData = ExecutionLib.encodeSingle( + address(coldStorageHook), + 0, + abi.encodeWithSelector( + ColdStorageHook.requestTimelockedExecution.selector, exec, additionalDelay + ) + ); + + UserOpData memory userOpData = mainAccount.getExecOps({ + target: address(coldStorageExecutor), + value: 0, + callData: abi.encodeWithSelector( + ColdStorageExecutor.executeOnSubAccount.selector, + address(coldStorage.account), + subAccountCallData + ), + txValidator: address(ownableValidator) + }); + + bytes memory signature = signHash(owner.key, userOpData.userOpHash); + address recover = + ECDSA.recover(ECDSA.toEthSignedMessageHash(userOpData.userOpHash), signature); + assertEq(recover, owner.addr); + userOpData.userOp.signature = signature; + userOpData.execUserOps(); + } + + function _execWithdraw(Execution memory exec) internal { + bytes memory subAccountCallData = + ExecutionLib.encodeSingle(exec.target, exec.value, exec.callData); + + UserOpData memory userOpData = mainAccount.getExecOps({ + target: address(coldStorageExecutor), + value: 0, + callData: abi.encodeWithSelector( + ColdStorageExecutor.executeOnSubAccount.selector, + address(coldStorage.account), + subAccountCallData + ), + txValidator: address(ownableValidator) + }); + bytes memory signature = signHash(owner.key, userOpData.userOpHash); + userOpData.userOp.signature = signature; + userOpData.execUserOps(); + } + + function test_withdraw() public { + uint256 prevBalance = token.balanceOf(address(mainAccount.account)); + uint256 amountToWithdraw = 100; + + _deploySubAccount(); + + Execution memory action = Execution({ + target: address(token), + value: 0, + callData: abi.encodeWithSelector( + MockERC20.transfer.selector, address(mainAccount.account), amountToWithdraw + ) + }); + + _requestWithdraw(action, 0); + + coldStorageHook.setWaitPeriod(7 days); + + vm.warp(block.timestamp + 8 days); + _execWithdraw(action); + + uint256 newBalance = token.balanceOf(address(mainAccount.account)); + assertEq(newBalance, prevBalance + amountToWithdraw); + } + + function test_setWaitPeriod() public { + _deploySubAccount(); + + uint256 newWaitPeriod = 2 days; + + Execution memory action = Execution({ + target: address(coldStorageHook), + value: 0, + callData: abi.encodeWithSelector(ColdStorageHook.setWaitPeriod.selector, (newWaitPeriod)) + }); + + _requestWithdraw(action, 0); + (bytes32 hash, bytes32 entry) = + coldStorageHook.checkHash(address(mainAccount.account), action); + + vm.warp(block.timestamp + 8 days); + _execWithdraw(action); + + Execution memory newAction = Execution({ + target: address(token), + value: 0, + callData: abi.encodeWithSelector( + MockERC20.transfer.selector, address(mainAccount.account), 100 + ) + }); + + _requestWithdraw(newAction, 0); + + vm.warp(block.timestamp + newWaitPeriod); + _execWithdraw(newAction); + + uint256 updatedWaitPeriod = coldStorageHook.getLockTime(address(coldStorage.account)); + assertEq(updatedWaitPeriod, uint128(newWaitPeriod)); + } + + function test_withdraw__With__NativeToken() public { + address target = address(mainAccount.account); + uint256 prevBalance = target.balance; + uint256 amountToWithdraw = 1 ether; + + vm.deal(address(coldStorage.account), 10 ether); + _deploySubAccount(); + + Execution memory action = + Execution({ target: target, value: amountToWithdraw, callData: "" }); + + _requestWithdraw(action, 0); + + vm.warp(block.timestamp + 8 days); + _execWithdraw(action); + + uint256 newBalance = target.balance; + assertTrue(newBalance > prevBalance); + } +} From 4a15b3938dc3f01e0cb316a8029d6a1e82289591 Mon Sep 17 00:00:00 2001 From: kopy-kat Date: Sat, 24 Feb 2024 15:57:42 +0000 Subject: [PATCH 2/9] feat: implement prototype and initial test --- .../src/PermissionsHook/PermissionsHook.sol | 60 ++- .../PermissionsHook/PermissionsHook.t.sol | 365 +++++------------- packages/modulekit/src/mocks/MockExecutor.sol | 12 +- 3 files changed, 158 insertions(+), 279 deletions(-) diff --git a/examples/src/PermissionsHook/PermissionsHook.sol b/examples/src/PermissionsHook/PermissionsHook.sol index 1573f348..f3541d00 100644 --- a/examples/src/PermissionsHook/PermissionsHook.sol +++ b/examples/src/PermissionsHook/PermissionsHook.sol @@ -12,8 +12,8 @@ contract PermissionsHook is ERC7579HookDestruct { CONSTANTS //////////////////////////////////////////////////////////////////////////*/ - bytes1 internal constant PERMISSION_DISALLOWED = 0x00; - bytes1 internal constant PERMISSION_ALLOWED = 0x00; + bytes1 internal constant FALSE_CONSTANT = 0x00; + bytes1 internal constant TRUE_CONSTANT = 0x01; error InvalidPermission(); @@ -22,13 +22,17 @@ contract PermissionsHook is ERC7579HookDestruct { // - Target permissions bytes1 selfCall; // 0x00 - false, 0x01 - true bytes1 moduleCall; // 0x00 - false, 0x01 - true + bytes1 hasAllowedTargets; // 0x00 - false, 0x01 - true // - Value permissions bytes1 sendValue; // 0x00 - false, 0x01 - true // - Calldata permissions + bytes1 hasAllowedFunctions; // 0x00 - false, 0x01 - true bytes1 erc20Transfer; // 0x00 - false, 0x01 - true bytes1 erc721Transfer; // 0x00 - false, 0x01 - true // Module configuration permissions bytes1 moduleConfig; // 0x00 - false, 0x01 - true + bytes4[] allowedFunctions; + address[] allowedTargets; } mapping(address account => mapping(address module => ModulePermissions)) internal permissions; @@ -113,7 +117,6 @@ contract PermissionsHook is ERC7579HookDestruct { returns (bytes memory hookData) { // Not callable from module - return ""; } function onExecuteBatch( @@ -126,7 +129,6 @@ contract PermissionsHook is ERC7579HookDestruct { returns (bytes memory hookData) { // Not callable from module - return ""; } function onExecuteFromExecutor( @@ -184,7 +186,7 @@ contract PermissionsHook is ERC7579HookDestruct { ModulePermissions storage modulePermissions = permissions[msg.sender][msgSender]; - if (modulePermissions.moduleConfig != PERMISSION_ALLOWED) { + if (modulePermissions.moduleConfig != TRUE_CONSTANT) { revert InvalidPermission(); } } @@ -210,7 +212,7 @@ contract PermissionsHook is ERC7579HookDestruct { ModulePermissions storage modulePermissions = permissions[msg.sender][msgSender]; - if (modulePermissions.moduleConfig != PERMISSION_ALLOWED) { + if (modulePermissions.moduleConfig != TRUE_CONSTANT) { revert InvalidPermission(); } } @@ -228,29 +230,59 @@ contract PermissionsHook is ERC7579HookDestruct { internal { // Target permissions - if (target == msg.sender && modulePermissions.selfCall != PERMISSION_ALLOWED) { + if (target == msg.sender && modulePermissions.selfCall != TRUE_CONSTANT) { revert InvalidPermission(); } - if (modulePermissions.moduleCall != PERMISSION_ALLOWED) { + if (modulePermissions.moduleCall != TRUE_CONSTANT) { if (IERC7579Account(msg.sender).isModuleInstalled(TYPE_EXECUTOR, target, "")) { revert InvalidPermission(); } } + if (modulePermissions.hasAllowedTargets == TRUE_CONSTANT) { + bool isAllowedTarget = false; + uint256 allowedTargetsLength = modulePermissions.allowedTargets.length; + for (uint256 i = 0; i < allowedTargetsLength; i++) { + if (modulePermissions.allowedTargets[i] == target) { + isAllowedTarget = true; + break; + } + } + + if (!isAllowedTarget) { + revert InvalidPermission(); + } + } + // Value permissions - if (value > 0 && modulePermissions.sendValue != PERMISSION_ALLOWED) { + if (value > 0 && modulePermissions.sendValue != TRUE_CONSTANT) { revert InvalidPermission(); } // Calldata permissions - if (_isErc20Transfer(callData) && modulePermissions.erc20Transfer != PERMISSION_ALLOWED) { + if (_isErc20Transfer(callData) && modulePermissions.erc20Transfer != TRUE_CONSTANT) { revert InvalidPermission(); } - if (_isErc721Transfer(callData) && modulePermissions.erc721Transfer != PERMISSION_ALLOWED) { + if (_isErc721Transfer(callData) && modulePermissions.erc721Transfer != TRUE_CONSTANT) { revert InvalidPermission(); } + + if (modulePermissions.hasAllowedFunctions == TRUE_CONSTANT) { + bool isAllowedFunction = false; + uint256 allowedFunctionsLength = modulePermissions.allowedFunctions.length; + for (uint256 i = 0; i < allowedFunctionsLength; i++) { + if (modulePermissions.allowedFunctions[i] == bytes4(callData[0:4])) { + isAllowedFunction = true; + break; + } + } + + if (!isAllowedFunction) { + revert InvalidPermission(); + } + } } function _isErc20Transfer(bytes calldata callData) @@ -258,6 +290,9 @@ contract PermissionsHook is ERC7579HookDestruct { pure returns (bool isErc20Transfer) { + if (callData.length < 4) { + return false; + } bytes4 functionSig = bytes4(callData[0:4]); if (functionSig == IERC20.transfer.selector || functionSig == IERC20.transferFrom.selector) { @@ -270,6 +305,9 @@ contract PermissionsHook is ERC7579HookDestruct { pure returns (bool isErc721Transfer) { + if (callData.length < 4) { + return false; + } bytes4 functionSig = bytes4(callData[0:4]); if (functionSig == IERC721.transferFrom.selector) { isErc721Transfer = true; diff --git a/examples/test/PermissionsHook/PermissionsHook.t.sol b/examples/test/PermissionsHook/PermissionsHook.t.sol index 457daa7f..994fc7a5 100644 --- a/examples/test/PermissionsHook/PermissionsHook.t.sol +++ b/examples/test/PermissionsHook/PermissionsHook.t.sol @@ -3,311 +3,142 @@ pragma solidity ^0.8.23; import "forge-std/Test.sol"; import "@rhinestone/modulekit/src/ModuleKit.sol"; -import "@rhinestone/modulekit/src/Modules.sol"; -import "@rhinestone/modulekit/src/Helpers.sol"; -import "@rhinestone/modulekit/src/core/ExtensibleFallbackHandler.sol"; -import "@rhinestone/sessionkeymanager/src/ISessionValidationModule.sol"; import { SessionData, SessionKeyManagerLib } from "@rhinestone/sessionkeymanager/src/SessionKeyManagerLib.sol"; -import "@rhinestone/modulekit/src/Mocks.sol"; +import { MockExecutor } from "@rhinestone/modulekit/src/Mocks.sol"; import { Solarray } from "solarray/Solarray.sol"; -import { ECDSA } from "solady/src/utils/ECDSA.sol"; -import { IERC7579Account, Execution } from "@rhinestone/modulekit/src/Accounts.sol"; -import { FlashloanCallback } from "src/ColdStorage/FlashloanCallback.sol"; -import { FlashloanLender } from "src/ColdStorage/FlashloanLender.sol"; -import { ColdStorageHook } from "src/ColdStorage/ColdStorageHook.sol"; -import { ColdStorageExecutor } from "src/ColdStorage/ColdStorageExecutor.sol"; -import { OwnableValidator } from "src/OwnableValidator/OwnableValidator.sol"; - -import { ERC7579BootstrapConfig } from "@rhinestone/modulekit/src/external/ERC7579.sol"; - -import "src/ColdStorage/interfaces/Flashloan.sol"; -import "erc7579/lib/ExecutionLib.sol"; +import { + MODULE_TYPE_HOOK, MODULE_TYPE_EXECUTOR +} from "@rhinestone/modulekit/src/external/ERC7579.sol"; +import { PermissionsHook, IERC7579Account } from "src/PermissionsHook/PermissionsHook.sol"; -contract ColdStorageTest is RhinestoneModuleKit, Test { +contract PermissionsHookTest is RhinestoneModuleKit, Test { using ModuleKitHelpers for *; using ModuleKitUserOp for *; - using ECDSA for bytes32; - - MockERC20 internal token; - // main account and dependencies - AccountInstance internal mainAccount; - FlashloanCallback internal flashloanCallback; + bytes1 internal constant FALSE_CONSTANT = 0x00; + bytes1 internal constant TRUE_CONSTANT = 0x01; - // ColdStorage Account and dependencies - AccountInstance internal coldStorage; - FlashloanLender internal flashloanLender; - ColdStorageHook internal coldStorageHook; - ColdStorageExecutor internal coldStorageExecutor; - OwnableValidator internal ownableValidator; + // Account instance and hook + AccountInstance internal instance; + PermissionsHook internal permissionsHook; - MockValidator internal mockValidator; + // Mock executors + MockExecutor internal executorDisallowed; + MockExecutor internal executorAllowed; - Account owner; + address activeExecutor; + bool activeCallSuccess; function setUp() public { init(); - flashloanLender = new FlashloanLender(address(coldStorage.aux.fallbackHandler)); - vm.label(address(flashloanLender), "flashloanLender"); - flashloanCallback = new FlashloanCallback(address(mainAccount.aux.fallbackHandler)); - vm.label(address(flashloanCallback), "flashloanCallback"); - ownableValidator = new OwnableValidator(); - vm.label(address(ownableValidator), "ownableValidator"); - mockValidator = new MockValidator(); - vm.label(address(mockValidator), "mockValidator"); - - coldStorageHook = new ColdStorageHook(); - vm.label(address(coldStorageHook), "coldStorageHook"); - - coldStorageExecutor = new ColdStorageExecutor(); - vm.label(address(coldStorageExecutor), "coldStorageExecutor"); - - owner = makeAccount("owner"); - _setupMainAccount(); - _setUpColdstorage(); - - deal(address(coldStorage.account), 100 ether); - deal(address(mainAccount.account), 100 ether); + permissionsHook = new PermissionsHook(); + vm.label(address(permissionsHook), "permissionsHook"); + executorDisallowed = new MockExecutor(); + vm.label(address(executorDisallowed), "executorDisallowed"); + executorAllowed = new MockExecutor(); + vm.label(address(executorAllowed), "executorAllowed"); - token = new MockERC20(); - token.initialize("Mock Token", "MTK", 18); - deal(address(token), mainAccount.account, 100 ether); + instance = makeAccountInstance("PermissionsHookTestAccount"); + deal(address(instance.account), 100 ether); - vm.warp(1_799_999); - - mainAccount.exec({ - target: address(token), - value: 0, - callData: abi.encodeCall(IERC20.transfer, (address(coldStorage.account), 1 ether)) - }); + setUpPermissionsHook(); } - function _setupMainAccount() public { - ExtensibleFallbackHandler.Params[] memory params = new ExtensibleFallbackHandler.Params[](1); - params[0] = ExtensibleFallbackHandler.Params({ - selector: IERC3156FlashBorrower.onFlashLoan.selector, - fallbackType: ExtensibleFallbackHandler.FallBackType.Dynamic, - handler: address(flashloanCallback) - }); - - ERC7579BootstrapConfig[] memory validators = - makeBootstrapConfig(address(ownableValidator), abi.encode(owner.addr)); - ERC7579BootstrapConfig[] memory executors = - makeBootstrapConfig(address(flashloanCallback), abi.encode("")); - ERC7579BootstrapConfig memory hook = _emptyConfig(); - ERC7579BootstrapConfig memory fallBack = - _makeBootstrapConfig(address(auxiliary.fallbackHandler), abi.encode(params)); - mainAccount = makeAccountInstance("mainAccount", validators, executors, hook, fallBack); - } - - function _setUpColdstorage() public { - ExtensibleFallbackHandler.Params[] memory params = new ExtensibleFallbackHandler.Params[](6); - params[0] = ExtensibleFallbackHandler.Params({ - selector: IERC3156FlashLender.maxFlashLoan.selector, - fallbackType: ExtensibleFallbackHandler.FallBackType.Static, - handler: address(flashloanLender) - }); - params[1] = ExtensibleFallbackHandler.Params({ - selector: IERC3156FlashLender.flashFee.selector, - fallbackType: ExtensibleFallbackHandler.FallBackType.Static, - handler: address(flashloanLender) + function setUpPermissionsHook() internal { + instance.installModule({ + moduleTypeId: MODULE_TYPE_EXECUTOR, + module: address(executorDisallowed), + data: "" }); - params[2] = ExtensibleFallbackHandler.Params({ - selector: IERC3156FlashLender.flashLoan.selector, - fallbackType: ExtensibleFallbackHandler.FallBackType.Dynamic, - handler: address(flashloanLender) + instance.installModule({ + moduleTypeId: MODULE_TYPE_EXECUTOR, + module: address(executorAllowed), + data: "" }); - params[3] = ExtensibleFallbackHandler.Params({ - selector: IERC6682.flashFeeToken.selector, - fallbackType: ExtensibleFallbackHandler.FallBackType.Static, - handler: address(flashloanLender) - }); - params[4] = ExtensibleFallbackHandler.Params({ - selector: IERC6682.flashFee.selector, - fallbackType: ExtensibleFallbackHandler.FallBackType.Static, - handler: address(flashloanLender) - }); - params[5] = ExtensibleFallbackHandler.Params({ - selector: IERC6682.availableForFlashLoan.selector, - fallbackType: ExtensibleFallbackHandler.FallBackType.Static, - handler: address(flashloanLender) - }); - - ERC7579BootstrapConfig[] memory validators = - makeBootstrapConfig(address(ownableValidator), abi.encode(address(mainAccount.account))); - - address[] memory addresses = new address[](2); - bytes[] memory callData = new bytes[](2); - - addresses[0] = address(flashloanLender); - addresses[1] = address(coldStorageExecutor); - - callData[0] = abi.encode(""); - callData[1] = abi.encodePacked(address(mainAccount.account)); - - ERC7579BootstrapConfig[] memory executors = makeBootstrapConfig(addresses, callData); - ERC7579BootstrapConfig memory hook = _makeBootstrapConfig( - address(coldStorageHook), abi.encode(uint128(7 days), address(mainAccount.account)) - ); - ERC7579BootstrapConfig memory fallBack = - _makeBootstrapConfig(address(auxiliary.fallbackHandler), abi.encode(params)); - - coldStorage = makeAccountInstance("coldStorage", validators, executors, hook, fallBack); - } - - function simulateDeposit() internal { - vm.prank(mainAccount.account); - token.transfer(coldStorage.account, 1 ether); - } - - function signHash(uint256 privKey, bytes32 digest) internal returns (bytes memory) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privKey, ECDSA.toEthSignedMessageHash(digest)); - return abi.encodePacked(r, s, v); - } - - function _deploySubAccount() private { - // todo: replace with modulekit function to deploy account - // create and exec an empty user op to deploy the sub account - UserOpData memory userOpData = - coldStorage.getExecOps(address(0), 0, "", address(ownableValidator)); - - bytes memory signature = signHash(owner.key, userOpData.userOpHash); - signature = abi.encodePacked(address(ownableValidator), signature); - userOpData.userOp.signature = signature; - coldStorage.expect4337Revert(); - userOpData.execUserOps(); - } - - function _requestWithdraw(Execution memory exec, uint256 additionalDelay) internal { - bytes memory subAccountCallData = ExecutionLib.encodeSingle( - address(coldStorageHook), - 0, - abi.encodeWithSelector( - ColdStorageHook.requestTimelockedExecution.selector, exec, additionalDelay - ) - ); - - UserOpData memory userOpData = mainAccount.getExecOps({ - target: address(coldStorageExecutor), - value: 0, - callData: abi.encodeWithSelector( - ColdStorageExecutor.executeOnSubAccount.selector, - address(coldStorage.account), - subAccountCallData - ), - txValidator: address(ownableValidator) + address[] memory executors = new address[](2); + executors[0] = address(executorDisallowed); + executors[1] = address(executorAllowed); + + PermissionsHook.ModulePermissions[] memory permissions = + new PermissionsHook.ModulePermissions[](2); + permissions[0] = PermissionsHook.ModulePermissions({ + selfCall: FALSE_CONSTANT, + moduleCall: FALSE_CONSTANT, + hasAllowedTargets: TRUE_CONSTANT, + sendValue: FALSE_CONSTANT, + hasAllowedFunctions: TRUE_CONSTANT, + erc20Transfer: FALSE_CONSTANT, + erc721Transfer: FALSE_CONSTANT, + moduleConfig: FALSE_CONSTANT, + allowedFunctions: new bytes4[](0), + allowedTargets: new address[](0) }); - bytes memory signature = signHash(owner.key, userOpData.userOpHash); - address recover = - ECDSA.recover(ECDSA.toEthSignedMessageHash(userOpData.userOpHash), signature); - assertEq(recover, owner.addr); - userOpData.userOp.signature = signature; - userOpData.execUserOps(); - } - - function _execWithdraw(Execution memory exec) internal { - bytes memory subAccountCallData = - ExecutionLib.encodeSingle(exec.target, exec.value, exec.callData); - - UserOpData memory userOpData = mainAccount.getExecOps({ - target: address(coldStorageExecutor), - value: 0, - callData: abi.encodeWithSelector( - ColdStorageExecutor.executeOnSubAccount.selector, - address(coldStorage.account), - subAccountCallData - ), - txValidator: address(ownableValidator) + permissions[1] = PermissionsHook.ModulePermissions({ + selfCall: TRUE_CONSTANT, + moduleCall: TRUE_CONSTANT, + hasAllowedTargets: FALSE_CONSTANT, + sendValue: TRUE_CONSTANT, + hasAllowedFunctions: FALSE_CONSTANT, + erc20Transfer: TRUE_CONSTANT, + erc721Transfer: TRUE_CONSTANT, + moduleConfig: TRUE_CONSTANT, + allowedFunctions: new bytes4[](0), + allowedTargets: new address[](0) }); - bytes memory signature = signHash(owner.key, userOpData.userOpHash); - userOpData.userOp.signature = signature; - userOpData.execUserOps(); - } - - function test_withdraw() public { - uint256 prevBalance = token.balanceOf(address(mainAccount.account)); - uint256 amountToWithdraw = 100; - - _deploySubAccount(); - Execution memory action = Execution({ - target: address(token), - value: 0, - callData: abi.encodeWithSelector( - MockERC20.transfer.selector, address(mainAccount.account), amountToWithdraw - ) + instance.installModule({ + moduleTypeId: MODULE_TYPE_HOOK, + module: address(permissionsHook), + data: abi.encode(executors, permissions) }); - - _requestWithdraw(action, 0); - - coldStorageHook.setWaitPeriod(7 days); - - vm.warp(block.timestamp + 8 days); - _execWithdraw(action); - - uint256 newBalance = token.balanceOf(address(mainAccount.account)); - assertEq(newBalance, prevBalance + amountToWithdraw); } - function test_setWaitPeriod() public { - _deploySubAccount(); - - uint256 newWaitPeriod = 2 days; - - Execution memory action = Execution({ - target: address(coldStorageHook), - value: 0, - callData: abi.encodeWithSelector(ColdStorageHook.setWaitPeriod.selector, (newWaitPeriod)) - }); - - _requestWithdraw(action, 0); - (bytes32 hash, bytes32 entry) = - coldStorageHook.checkHash(address(mainAccount.account), action); - - vm.warp(block.timestamp + 8 days); - _execWithdraw(action); - - Execution memory newAction = Execution({ - target: address(token), - value: 0, - callData: abi.encodeWithSelector( - MockERC20.transfer.selector, address(mainAccount.account), 100 - ) - }); - - _requestWithdraw(newAction, 0); + modifier performWithBothExecutors() { + // Disallowed executor + activeExecutor = address(executorDisallowed); + _; + assertFalse(activeCallSuccess); - vm.warp(block.timestamp + newWaitPeriod); - _execWithdraw(newAction); - - uint256 updatedWaitPeriod = coldStorageHook.getLockTime(address(coldStorage.account)); - assertEq(updatedWaitPeriod, uint128(newWaitPeriod)); + // Allowed executor + activeExecutor = address(executorAllowed); + _; + assertTrue(activeCallSuccess); } - function test_withdraw__With__NativeToken() public { - address target = address(mainAccount.account); - uint256 prevBalance = target.balance; - uint256 amountToWithdraw = 1 ether; + function test_selfCall() public performWithBothExecutors { + address target = instance.account; + uint256 value = 0; + bytes memory callData = abi.encodeWithSelector( + IERC7579Account.execute.selector, + bytes32(0), + abi.encodePacked(makeAddr("target"), uint256(1 ether), bytes("")) + ); - vm.deal(address(coldStorage.account), 10 ether); - _deploySubAccount(); + bytes memory executorCallData = abi.encodeWithSelector( + MockExecutor.exec.selector, instance.account, target, value, callData + ); - Execution memory action = - Execution({ target: target, value: amountToWithdraw, callData: "" }); + (bool success, bytes memory result) = activeExecutor.call(executorCallData); + activeCallSuccess = success; + } - _requestWithdraw(action, 0); + function test_sendValue() public performWithBothExecutors { + address target = makeAddr("target"); + uint256 value = 1 ether; + bytes memory callData = ""; - vm.warp(block.timestamp + 8 days); - _execWithdraw(action); + bytes memory executorCallData = abi.encodeWithSelector( + MockExecutor.exec.selector, instance.account, target, value, callData + ); - uint256 newBalance = target.balance; - assertTrue(newBalance > prevBalance); + (bool success, bytes memory result) = activeExecutor.call(executorCallData); + activeCallSuccess = success; } } diff --git a/packages/modulekit/src/mocks/MockExecutor.sol b/packages/modulekit/src/mocks/MockExecutor.sol index 9fb9b3c7..b9982f5f 100644 --- a/packages/modulekit/src/mocks/MockExecutor.sol +++ b/packages/modulekit/src/mocks/MockExecutor.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.23; import { ERC7579ExecutorBase } from "../Modules.sol"; -import { IERC7579Account } from "../external/ERC7579.sol"; +import { IERC7579Account, Execution } from "../external/ERC7579.sol"; contract MockExecutor is ERC7579ExecutorBase { function onInstall(bytes calldata data) external override { } @@ -21,6 +21,16 @@ contract MockExecutor is ERC7579ExecutorBase { return _execute(account, to, value, callData); } + function execBatch( + address account, + Execution[] memory execs + ) + external + returns (bytes[] memory) + { + return _execute(account, execs); + } + function isModuleType(uint256 typeID) external pure override returns (bool) { return typeID == TYPE_EXECUTOR; } From 3ed365d6562c91617bcb304f642b91b8dd6c59e5 Mon Sep 17 00:00:00 2001 From: kopy-kat Date: Sat, 24 Feb 2024 18:18:12 +0000 Subject: [PATCH 3/9] fix: use bools --- .../src/PermissionsHook/PermissionsHook.sol | 37 +++++++++---------- .../PermissionsHook/PermissionsHook.t.sol | 35 ++++++++---------- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/examples/src/PermissionsHook/PermissionsHook.sol b/examples/src/PermissionsHook/PermissionsHook.sol index f3541d00..e42f2ac0 100644 --- a/examples/src/PermissionsHook/PermissionsHook.sol +++ b/examples/src/PermissionsHook/PermissionsHook.sol @@ -12,25 +12,22 @@ contract PermissionsHook is ERC7579HookDestruct { CONSTANTS //////////////////////////////////////////////////////////////////////////*/ - bytes1 internal constant FALSE_CONSTANT = 0x00; - bytes1 internal constant TRUE_CONSTANT = 0x01; - error InvalidPermission(); struct ModulePermissions { // Execution permissions // - Target permissions - bytes1 selfCall; // 0x00 - false, 0x01 - true - bytes1 moduleCall; // 0x00 - false, 0x01 - true - bytes1 hasAllowedTargets; // 0x00 - false, 0x01 - true + bool selfCall; + bool moduleCall; + bool hasAllowedTargets; // - Value permissions - bytes1 sendValue; // 0x00 - false, 0x01 - true + bool sendValue; // - Calldata permissions - bytes1 hasAllowedFunctions; // 0x00 - false, 0x01 - true - bytes1 erc20Transfer; // 0x00 - false, 0x01 - true - bytes1 erc721Transfer; // 0x00 - false, 0x01 - true + bool hasAllowedFunctions; + bool erc20Transfer; + bool erc721Transfer; // Module configuration permissions - bytes1 moduleConfig; // 0x00 - false, 0x01 - true + bool moduleConfig; bytes4[] allowedFunctions; address[] allowedTargets; } @@ -186,7 +183,7 @@ contract PermissionsHook is ERC7579HookDestruct { ModulePermissions storage modulePermissions = permissions[msg.sender][msgSender]; - if (modulePermissions.moduleConfig != TRUE_CONSTANT) { + if (!modulePermissions.moduleConfig) { revert InvalidPermission(); } } @@ -212,7 +209,7 @@ contract PermissionsHook is ERC7579HookDestruct { ModulePermissions storage modulePermissions = permissions[msg.sender][msgSender]; - if (modulePermissions.moduleConfig != TRUE_CONSTANT) { + if (!modulePermissions.moduleConfig) { revert InvalidPermission(); } } @@ -230,17 +227,17 @@ contract PermissionsHook is ERC7579HookDestruct { internal { // Target permissions - if (target == msg.sender && modulePermissions.selfCall != TRUE_CONSTANT) { + if (target == msg.sender && !modulePermissions.selfCall) { revert InvalidPermission(); } - if (modulePermissions.moduleCall != TRUE_CONSTANT) { + if (!modulePermissions.moduleCall) { if (IERC7579Account(msg.sender).isModuleInstalled(TYPE_EXECUTOR, target, "")) { revert InvalidPermission(); } } - if (modulePermissions.hasAllowedTargets == TRUE_CONSTANT) { + if (modulePermissions.hasAllowedTargets) { bool isAllowedTarget = false; uint256 allowedTargetsLength = modulePermissions.allowedTargets.length; for (uint256 i = 0; i < allowedTargetsLength; i++) { @@ -256,20 +253,20 @@ contract PermissionsHook is ERC7579HookDestruct { } // Value permissions - if (value > 0 && modulePermissions.sendValue != TRUE_CONSTANT) { + if (value > 0 && !modulePermissions.sendValue) { revert InvalidPermission(); } // Calldata permissions - if (_isErc20Transfer(callData) && modulePermissions.erc20Transfer != TRUE_CONSTANT) { + if (_isErc20Transfer(callData) && !modulePermissions.erc20Transfer) { revert InvalidPermission(); } - if (_isErc721Transfer(callData) && modulePermissions.erc721Transfer != TRUE_CONSTANT) { + if (_isErc721Transfer(callData) && !modulePermissions.erc721Transfer) { revert InvalidPermission(); } - if (modulePermissions.hasAllowedFunctions == TRUE_CONSTANT) { + if (modulePermissions.hasAllowedFunctions) { bool isAllowedFunction = false; uint256 allowedFunctionsLength = modulePermissions.allowedFunctions.length; for (uint256 i = 0; i < allowedFunctionsLength; i++) { diff --git a/examples/test/PermissionsHook/PermissionsHook.t.sol b/examples/test/PermissionsHook/PermissionsHook.t.sol index 994fc7a5..c2ee6e9c 100644 --- a/examples/test/PermissionsHook/PermissionsHook.t.sol +++ b/examples/test/PermissionsHook/PermissionsHook.t.sol @@ -19,9 +19,6 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { using ModuleKitHelpers for *; using ModuleKitUserOp for *; - bytes1 internal constant FALSE_CONSTANT = 0x00; - bytes1 internal constant TRUE_CONSTANT = 0x01; - // Account instance and hook AccountInstance internal instance; PermissionsHook internal permissionsHook; @@ -68,27 +65,27 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { PermissionsHook.ModulePermissions[] memory permissions = new PermissionsHook.ModulePermissions[](2); permissions[0] = PermissionsHook.ModulePermissions({ - selfCall: FALSE_CONSTANT, - moduleCall: FALSE_CONSTANT, - hasAllowedTargets: TRUE_CONSTANT, - sendValue: FALSE_CONSTANT, - hasAllowedFunctions: TRUE_CONSTANT, - erc20Transfer: FALSE_CONSTANT, - erc721Transfer: FALSE_CONSTANT, - moduleConfig: FALSE_CONSTANT, + selfCall: false, + moduleCall: false, + hasAllowedTargets: true, + sendValue: false, + hasAllowedFunctions: true, + erc20Transfer: false, + erc721Transfer: false, + moduleConfig: false, allowedFunctions: new bytes4[](0), allowedTargets: new address[](0) }); permissions[1] = PermissionsHook.ModulePermissions({ - selfCall: TRUE_CONSTANT, - moduleCall: TRUE_CONSTANT, - hasAllowedTargets: FALSE_CONSTANT, - sendValue: TRUE_CONSTANT, - hasAllowedFunctions: FALSE_CONSTANT, - erc20Transfer: TRUE_CONSTANT, - erc721Transfer: TRUE_CONSTANT, - moduleConfig: TRUE_CONSTANT, + selfCall: true, + moduleCall: true, + hasAllowedTargets: false, + sendValue: true, + hasAllowedFunctions: false, + erc20Transfer: true, + erc721Transfer: true, + moduleConfig: true, allowedFunctions: new bytes4[](0), allowedTargets: new address[](0) }); From 06960099136953717a3a1f1a4b5aa3c4b9e53a77 Mon Sep 17 00:00:00 2001 From: zeroknots Date: Mon, 26 Feb 2024 11:56:20 +0700 Subject: [PATCH 4/9] prototype --- accounts/safe7579/package.json | 2 +- examples/package.json | 2 +- examples/src/DeadmanSwitch/DeadmanSwitch.sol | 4 +- .../src/PermissionsHook/PermissionsHook.sol | 15 ++- examples/test/MultiFactor/MultiFactor.t.sol | 1 + .../PermissionsHook/PermissionsHook.t.sol | 37 +++++++- packages/SessionKeyManager/package.json | 2 +- packages/modulekit/package.json | 2 +- packages/modulekit/src/mocks/MockHook.sol | 4 +- .../src/modules/ERC7579HookDestruct.sol | 6 +- pnpm-lock.yaml | 91 ++++++++++++++++--- 11 files changed, 129 insertions(+), 37 deletions(-) diff --git a/accounts/safe7579/package.json b/accounts/safe7579/package.json index 6d0df445..6142b563 100644 --- a/accounts/safe7579/package.json +++ b/accounts/safe7579/package.json @@ -21,7 +21,7 @@ "@prb/math": "^4.0.2", "forge-std": "github:foundry-rs/forge-std", "ds-test": "github:dapphub/ds-test", - "erc7579": "github:erc7579/erc7579-implementation", + "erc7579": "github:erc7579/erc7579-implementation#feature/hookRevert", "sentinellist": "github:zeroknots/sentinellist", "solady": "github:vectorized/solady", "solarray": "github:sablier-labs/solarray", diff --git a/examples/package.json b/examples/package.json index a2cfb2c9..db51d98f 100644 --- a/examples/package.json +++ b/examples/package.json @@ -22,7 +22,7 @@ "@ERC4337/account-abstraction-v0.6": "github:eth-infinitism/account-abstraction#v0.6.0", "forge-std": "github:foundry-rs/forge-std", "ds-test": "github:dapphub/ds-test", - "erc7579": "github:erc7579/erc7579-implementation", + "erc7579": "github:erc7579/erc7579-implementation#feature/hookRevert", "sentinellist": "github:zeroknots/sentinellist", "solady": "github:vectorized/solady", "solarray": "github:sablier-labs/solarray", diff --git a/examples/src/DeadmanSwitch/DeadmanSwitch.sol b/examples/src/DeadmanSwitch/DeadmanSwitch.sol index 0eeee9b2..c0334dde 100644 --- a/examples/src/DeadmanSwitch/DeadmanSwitch.sol +++ b/examples/src/DeadmanSwitch/DeadmanSwitch.sol @@ -58,9 +58,7 @@ contract DeadmanSwitch is ERC7579HookBase, ERC7579ValidatorBase { config.lastAccess = uint48(block.timestamp); } - function postCheck(bytes calldata) external pure returns (bool success) { - success = true; - } + function postCheck(bytes calldata) external { } function validateUserOp( PackedUserOperation calldata userOp, diff --git a/examples/src/PermissionsHook/PermissionsHook.sol b/examples/src/PermissionsHook/PermissionsHook.sol index e42f2ac0..9e97e621 100644 --- a/examples/src/PermissionsHook/PermissionsHook.sol +++ b/examples/src/PermissionsHook/PermissionsHook.sol @@ -7,6 +7,8 @@ import { Execution, IERC7579Account } from "@rhinestone/modulekit/src/Accounts.s import { IERC721 } from "forge-std/interfaces/IERC721.sol"; import { IERC20 } from "forge-std/interfaces/IERC20.sol"; +import "forge-std/console2.sol"; + contract PermissionsHook is ERC7579HookDestruct { /*////////////////////////////////////////////////////////////////////////// CONSTANTS @@ -114,6 +116,9 @@ contract PermissionsHook is ERC7579HookDestruct { returns (bytes memory hookData) { // Not callable from module + ModulePermissions memory modulePermissions = permissions[msg.sender][msgSender]; + console2.log("modulePermissions", modulePermissions.moduleCall); + _validateExecutePermissions(modulePermissions, target, value, callData); } function onExecuteBatch( @@ -231,11 +236,11 @@ contract PermissionsHook is ERC7579HookDestruct { revert InvalidPermission(); } - if (!modulePermissions.moduleCall) { - if (IERC7579Account(msg.sender).isModuleInstalled(TYPE_EXECUTOR, target, "")) { - revert InvalidPermission(); - } - } + // if (!modulePermissions.moduleCall) { + // if (IERC7579Account(msg.sender).isModuleInstalled(TYPE_EXECUTOR, target, "")) { + // revert InvalidPermission(); + // } + // } if (modulePermissions.hasAllowedTargets) { bool isAllowedTarget = false; diff --git a/examples/test/MultiFactor/MultiFactor.t.sol b/examples/test/MultiFactor/MultiFactor.t.sol index c18b3b17..ef9f2f37 100644 --- a/examples/test/MultiFactor/MultiFactor.t.sol +++ b/examples/test/MultiFactor/MultiFactor.t.sol @@ -5,6 +5,7 @@ import "forge-std/Test.sol"; import "@rhinestone/modulekit/src/ModuleKit.sol"; import "@rhinestone/modulekit/src/Modules.sol"; import "@rhinestone/modulekit/src/Mocks.sol"; +import { EncodedModuleTypes } from "erc7579/lib/ModuleTypeLib.sol"; import { MultiFactor, ECDSAFactor } from "src/MultiFactor/MultiFactor.sol"; import { SignatureCheckerLib } from "solady/src/utils/SignatureCheckerLib.sol"; diff --git a/examples/test/PermissionsHook/PermissionsHook.t.sol b/examples/test/PermissionsHook/PermissionsHook.t.sol index c2ee6e9c..af618aae 100644 --- a/examples/test/PermissionsHook/PermissionsHook.t.sol +++ b/examples/test/PermissionsHook/PermissionsHook.t.sol @@ -47,6 +47,7 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { } function setUpPermissionsHook() internal { + console2.log("setting up permissions hook"); instance.installModule({ moduleTypeId: MODULE_TYPE_EXECUTOR, module: address(executorDisallowed), @@ -58,12 +59,13 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { data: "" }); - address[] memory executors = new address[](2); - executors[0] = address(executorDisallowed); - executors[1] = address(executorAllowed); + address[] memory modules = new address[](3); + modules[0] = address(executorDisallowed); + modules[1] = address(executorAllowed); + modules[2] = address(instance.defaultValidator); PermissionsHook.ModulePermissions[] memory permissions = - new PermissionsHook.ModulePermissions[](2); + new PermissionsHook.ModulePermissions[](3); permissions[0] = PermissionsHook.ModulePermissions({ selfCall: false, moduleCall: false, @@ -90,11 +92,26 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { allowedTargets: new address[](0) }); + permissions[2] = PermissionsHook.ModulePermissions({ + selfCall: true, + moduleCall: true, + hasAllowedTargets: false, + sendValue: true, + hasAllowedFunctions: false, + erc20Transfer: true, + erc721Transfer: true, + moduleConfig: true, + allowedFunctions: new bytes4[](0), + allowedTargets: new address[](0) + }); + + console2.log("installing module"); instance.installModule({ moduleTypeId: MODULE_TYPE_HOOK, module: address(permissionsHook), - data: abi.encode(executors, permissions) + data: abi.encode(modules, permissions) }); + console2.log("installed"); } modifier performWithBothExecutors() { @@ -138,4 +155,14 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { (bool success, bytes memory result) = activeExecutor.call(executorCallData); activeCallSuccess = success; } + + function test_sendValue_4337() public performWithBothExecutors { + address target = makeAddr("target"); + uint256 balanceBefore = target.balance; + uint256 value = 1 ether; + bytes memory callData = ""; + + instance.exec({ target: address(target), value: value, callData: callData }); + assertEq(target.balance, balanceBefore + value); + } } diff --git a/packages/SessionKeyManager/package.json b/packages/SessionKeyManager/package.json index 5371212a..0bb7a389 100644 --- a/packages/SessionKeyManager/package.json +++ b/packages/SessionKeyManager/package.json @@ -16,7 +16,7 @@ "@ERC4337/account-abstraction": "github:kopy-kat/account-abstraction#develop", "@ERC4337/account-abstraction-v0.6": "github:eth-infinitism/account-abstraction#v0.6.0", "sentinellist": "github:zeroknots/sentinellist", - "erc7579": "github:erc7579/erc7579-implementation", + "erc7579": "github:erc7579/erc7579-implementation#feature/hookRevert", "forge-std": "github:foundry-rs/forge-std", "solady": "github:vectorized/solady", "solhint": "^4.1.1", diff --git a/packages/modulekit/package.json b/packages/modulekit/package.json index d0da21e4..1b5c4ff8 100644 --- a/packages/modulekit/package.json +++ b/packages/modulekit/package.json @@ -22,7 +22,7 @@ "@ERC4337/account-abstraction": "github:kopy-kat/account-abstraction#develop", "@ERC4337/account-abstraction-v0.6": "github:eth-infinitism/account-abstraction#v0.6.0", "@safe-global/safe-contracts": "^1.4.1", - "erc7579": "github:erc7579/erc7579-implementation", + "erc7579": "github:erc7579/erc7579-implementation#feature/hookRevert", "prettier": "^2.8.8", "sentinellist": "github:zeroknots/sentinellist", "solady": "github:vectorized/solady", diff --git a/packages/modulekit/src/mocks/MockHook.sol b/packages/modulekit/src/mocks/MockHook.sol index e7ef64c9..4fac3fc2 100644 --- a/packages/modulekit/src/mocks/MockHook.sol +++ b/packages/modulekit/src/mocks/MockHook.sol @@ -18,9 +18,7 @@ contract MockHook is ERC7579HookBase { returns (bytes memory hookData) { } - function postCheck(bytes calldata) external virtual override returns (bool success) { - return true; - } + function postCheck(bytes calldata) external virtual override { } function isInitialized(address smartAccount) external pure returns (bool) { return false; diff --git a/packages/modulekit/src/modules/ERC7579HookDestruct.sol b/packages/modulekit/src/modules/ERC7579HookDestruct.sol index f50df6a6..923ddae1 100644 --- a/packages/modulekit/src/modules/ERC7579HookDestruct.sol +++ b/packages/modulekit/src/modules/ERC7579HookDestruct.sol @@ -147,9 +147,9 @@ abstract contract ERC7579HookDestruct is ERC7579HookBase { // revert HookInvalidSelector(); // } - function postCheck(bytes calldata hookData) external override returns (bool success) { - if (hookData.length == 0) return true; - return onPostCheck(hookData); + function postCheck(bytes calldata hookData) external override { + if (hookData.length == 0) return; + if (onPostCheck(hookData) == false) revert(); } /*////////////////////////////////////////////////////////////////////////// diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ed6ef18..c35bdd3e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,8 +54,8 @@ importers: specifier: github:rhinestonewtf/erc4337-validation version: github.com/rhinestonewtf/erc4337-validation/19a97d86f8f29709664334078925b2a843be19e0 erc7579: - specifier: github:erc7579/erc7579-implementation - version: github.com/erc7579/erc7579-implementation/1548b5bd9d20c257926d84952190cc6877ba7a57 + specifier: github:erc7579/erc7579-implementation#feature/hookRevert + version: github.com/erc7579/erc7579-implementation/db377a819c44515cb9454a9da151714136ee0b25 forge-std: specifier: github:foundry-rs/forge-std version: github.com/foundry-rs/forge-std/1d0766bc5d814f117c7b1e643828f7d85024fb51 @@ -82,7 +82,7 @@ importers: devDependencies: '@ERC4337/account-abstraction': specifier: github:kopy-kat/account-abstraction#develop - version: github.com/kopy-kat/account-abstraction/3c0127e9f7cb76e4a0fdb2d35cac51900f252bdc(ethers@5.4.0)(hardhat@2.20.1)(lodash@4.17.21)(typechain@5.2.0) + version: github.com/kopy-kat/account-abstraction/396bda190ddc00919339986c72c580c160a15587(ethers@5.4.0)(hardhat@2.20.1)(lodash@4.17.21)(typechain@5.2.0) '@ERC4337/account-abstraction-v0.6': specifier: github:eth-infinitism/account-abstraction#v0.6.0 version: github.com/eth-infinitism/account-abstraction/7174d6d845618dbd11cee68eefa715f5263690b6(ethers@5.4.0)(hardhat@2.20.1)(lodash@4.17.21)(typechain@5.2.0) @@ -109,10 +109,10 @@ importers: version: github.com/dapphub/ds-test/e282159d5170298eb2455a6c05280ab5a73a4ef0 erc4337-validation: specifier: github:rhinestonewtf/erc4337-validation - version: github.com/rhinestonewtf/erc4337-validation/19a97d86f8f29709664334078925b2a843be19e0 + version: git@github.com+rhinestonewtf/erc4337-validation/19a97d86f8f29709664334078925b2a843be19e0 erc7579: - specifier: github:erc7579/erc7579-implementation - version: github.com/erc7579/erc7579-implementation/1548b5bd9d20c257926d84952190cc6877ba7a57 + specifier: github:erc7579/erc7579-implementation#feature/hookRevert + version: github.com/erc7579/erc7579-implementation/cb007568e1c420ddbddf83f1880a4dc7c981e178 forge-std: specifier: github:foundry-rs/forge-std version: github.com/foundry-rs/forge-std/1d0766bc5d814f117c7b1e643828f7d85024fb51 @@ -124,7 +124,7 @@ importers: version: github.com/zeroknots/sentinellist/6294bf412489c0f6d3b9c92ad0aceb08c5b8704b solady: specifier: github:vectorized/solady - version: github.com/vectorized/solady/72e47ca417d24a30801b2921584e8486462cfc7b + version: github.com/vectorized/solady/21009ce09f02c0e20ce4750b63577e8c0cc7ced8 solarray: specifier: github:sablier-labs/solarray version: github.com/sablier-labs/solarray/6bf10cb34cdace52a3ba5fe437e78cc82df92684 @@ -150,8 +150,8 @@ importers: specifier: github:dapphub/ds-test version: github.com/dapphub/ds-test/e282159d5170298eb2455a6c05280ab5a73a4ef0 erc7579: - specifier: github:erc7579/erc7579-implementation - version: github.com/erc7579/erc7579-implementation/1548b5bd9d20c257926d84952190cc6877ba7a57 + specifier: github:erc7579/erc7579-implementation#feature/hookRevert + version: github.com/erc7579/erc7579-implementation/db377a819c44515cb9454a9da151714136ee0b25 forge-std: specifier: github:foundry-rs/forge-std version: github.com/foundry-rs/forge-std/1d0766bc5d814f117c7b1e643828f7d85024fb51 @@ -198,8 +198,8 @@ importers: specifier: github:rhinestonewtf/erc4337-validation version: github.com/rhinestonewtf/erc4337-validation/19a97d86f8f29709664334078925b2a843be19e0 erc7579: - specifier: github:erc7579/erc7579-implementation - version: github.com/erc7579/erc7579-implementation/1548b5bd9d20c257926d84952190cc6877ba7a57 + specifier: github:erc7579/erc7579-implementation#feature/hookRevert + version: github.com/erc7579/erc7579-implementation/8bdabebe50afeaed5d3c7916bfb98a5290de1dc3 forge-std: specifier: github:foundry-rs/forge-std version: github.com/foundry-rs/forge-std/1d0766bc5d814f117c7b1e643828f7d85024fb51 @@ -4456,14 +4456,39 @@ packages: ethers: 5.7.2 dev: true + git@github.com+rhinestonewtf/erc4337-validation/19a97d86f8f29709664334078925b2a843be19e0: + resolution: {commit: 19a97d86f8f29709664334078925b2a843be19e0, repo: git@github.com:rhinestonewtf/erc4337-validation.git, type: git} + name: erc4337-validation + version: 0.0.1 + dependencies: + '@openzeppelin/contracts': 5.0.1 + solady: github.com/vectorized/solady/21009ce09f02c0e20ce4750b63577e8c0cc7ced8 + dev: true + github.com/dapphub/ds-test/e282159d5170298eb2455a6c05280ab5a73a4ef0: resolution: {tarball: https://codeload.github.com/dapphub/ds-test/tar.gz/e282159d5170298eb2455a6c05280ab5a73a4ef0} name: ds-test version: 1.0.0 dev: true - github.com/erc7579/erc7579-implementation/1548b5bd9d20c257926d84952190cc6877ba7a57: - resolution: {tarball: https://codeload.github.com/erc7579/erc7579-implementation/tar.gz/1548b5bd9d20c257926d84952190cc6877ba7a57} + github.com/erc7579/erc7579-implementation/8bdabebe50afeaed5d3c7916bfb98a5290de1dc3: + resolution: {tarball: https://codeload.github.com/erc7579/erc7579-implementation/tar.gz/8bdabebe50afeaed5d3c7916bfb98a5290de1dc3} + name: micro-msa + version: 0.3.1 + dependencies: + '@openzeppelin/contracts': 5.0.1 + dev: true + + github.com/erc7579/erc7579-implementation/cb007568e1c420ddbddf83f1880a4dc7c981e178: + resolution: {tarball: https://codeload.github.com/erc7579/erc7579-implementation/tar.gz/cb007568e1c420ddbddf83f1880a4dc7c981e178} + name: micro-msa + version: 0.3.1 + dependencies: + '@openzeppelin/contracts': 5.0.1 + dev: true + + github.com/erc7579/erc7579-implementation/db377a819c44515cb9454a9da151714136ee0b25: + resolution: {tarball: https://codeload.github.com/erc7579/erc7579-implementation/tar.gz/db377a819c44515cb9454a9da151714136ee0b25} name: micro-msa version: 0.3.1 dependencies: @@ -4538,6 +4563,38 @@ packages: version: 1.7.6 dev: true + github.com/kopy-kat/account-abstraction/396bda190ddc00919339986c72c580c160a15587(ethers@5.4.0)(hardhat@2.20.1)(lodash@4.17.21)(typechain@5.2.0): + resolution: {tarball: https://codeload.github.com/kopy-kat/account-abstraction/tar.gz/396bda190ddc00919339986c72c580c160a15587} + id: github.com/kopy-kat/account-abstraction/396bda190ddc00919339986c72c580c160a15587 + name: accountabstraction + version: 0.7.0 + dependencies: + '@nomiclabs/hardhat-etherscan': 2.1.8(hardhat@2.20.1) + '@openzeppelin/contracts': 5.0.1 + '@thehubbleproject/bls': 0.5.1 + '@typechain/hardhat': 2.3.1(hardhat@2.20.1)(lodash@4.17.21)(typechain@5.2.0) + '@types/debug': 4.1.12 + '@types/mocha': 9.1.1 + debug: 4.3.4(supports-color@8.1.1) + ethereumjs-util: 7.1.5 + ethereumjs-wallet: 1.0.2 + hardhat-deploy: 0.11.45 + hardhat-deploy-ethers: 0.3.0-beta.13(ethers@5.4.0)(hardhat@2.20.1) + solidity-coverage: 0.8.8(hardhat@2.20.1) + source-map-support: 0.5.21 + table: 6.8.1 + typescript: 4.9.5 + transitivePeerDependencies: + - bufferutil + - encoding + - ethers + - hardhat + - lodash + - supports-color + - typechain + - utf-8-validate + dev: true + github.com/kopy-kat/account-abstraction/3c0127e9f7cb76e4a0fdb2d35cac51900f252bdc(ethers@5.4.0)(hardhat@2.20.1)(lodash@4.17.21)(typechain@5.2.0): resolution: {tarball: https://codeload.github.com/kopy-kat/account-abstraction/tar.gz/3c0127e9f7cb76e4a0fdb2d35cac51900f252bdc} id: github.com/kopy-kat/account-abstraction/3c0127e9f7cb76e4a0fdb2d35cac51900f252bdc @@ -4608,7 +4665,7 @@ packages: version: 0.0.1 dependencies: '@openzeppelin/contracts': 5.0.1 - solady: github.com/vectorized/solady/72e47ca417d24a30801b2921584e8486462cfc7b + solady: github.com/vectorized/solady/21009ce09f02c0e20ce4750b63577e8c0cc7ced8 dev: true github.com/sablier-labs/solarray/6bf10cb34cdace52a3ba5fe437e78cc82df92684: @@ -4623,6 +4680,12 @@ packages: version: 6.2.0 dev: true + github.com/vectorized/solady/21009ce09f02c0e20ce4750b63577e8c0cc7ced8: + resolution: {tarball: https://codeload.github.com/vectorized/solady/tar.gz/21009ce09f02c0e20ce4750b63577e8c0cc7ced8} + name: solady + version: 0.0.172 + dev: true + github.com/vectorized/solady/72e47ca417d24a30801b2921584e8486462cfc7b: resolution: {tarball: https://codeload.github.com/vectorized/solady/tar.gz/72e47ca417d24a30801b2921584e8486462cfc7b} name: solady From ab19dc6a79f016ea90b7da84c002870317864e03 Mon Sep 17 00:00:00 2001 From: zeroknots Date: Tue, 27 Feb 2024 13:41:05 +0700 Subject: [PATCH 5/9] adding subhooks and multiplexer --- examples/foundry.toml | 2 + .../src/PermissionsHook/HookMultiplexer.sol | 89 +++++ .../src/PermissionsHook/PermissionsHookV2.sol | 319 ++++++++++++++++++ .../TransactionDetectionLib.sol | 20 ++ .../subHooks/RegistryCheck.sol | 1 + .../subHooks/SpendingLimit.sol | 60 ++++ .../PermissionsHook/PermissionsHook.t.sol | 58 +++- packages/modulekit/foundry.toml | 2 + packages/modulekit/src/Modules.sol | 3 +- pnpm-lock.yaml | 29 +- 10 files changed, 550 insertions(+), 33 deletions(-) create mode 100644 examples/src/PermissionsHook/HookMultiplexer.sol create mode 100644 examples/src/PermissionsHook/PermissionsHookV2.sol create mode 100644 examples/src/PermissionsHook/TransactionDetectionLib.sol create mode 100644 examples/src/PermissionsHook/subHooks/RegistryCheck.sol create mode 100644 examples/src/PermissionsHook/subHooks/SpendingLimit.sol diff --git a/examples/foundry.toml b/examples/foundry.toml index 8e068472..350ed86c 100644 --- a/examples/foundry.toml +++ b/examples/foundry.toml @@ -5,6 +5,8 @@ out = "out" libs = ["lib"] fs_permissions = [{ access = "read", path = "out-optimized" }] allow_paths = ["*", "/"] +evm_version = "cancun" +ignored_error_codes = [2394] [fmt] bracket_spacing = true diff --git a/examples/src/PermissionsHook/HookMultiplexer.sol b/examples/src/PermissionsHook/HookMultiplexer.sol new file mode 100644 index 00000000..b6c0f5f7 --- /dev/null +++ b/examples/src/PermissionsHook/HookMultiplexer.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { Execution } from "@rhinestone/modulekit/src/Accounts.sol"; +import { SENTINEL, SentinelListLib } from "sentinellist/SentinelList.sol"; +import "forge-std/console2.sol"; + +abstract contract SubHook { + address public immutable HOOK_MULTIPLEXER; + + constructor(address hookMultiplexer) { + HOOK_MULTIPLEXER = hookMultiplexer; + } + + modifier onlyMultiPlexer() { + if (msg.sender != HOOK_MULTIPLEXER) { + revert("Unauthorized"); + } + _; + } + + function onExecute( + address smartAccount, + address module, + address target, + uint256 value, + bytes calldata callData + ) + external + virtual + returns (bytes memory); + + function onExecuteBatch( + address smartAccount, + address module, + Execution[] calldata executions + ) + external + virtual + returns (bytes memory); +} + +abstract contract HookMultiPlexer { + using SentinelListLib for SentinelListLib.SentinelList; + + 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; + for (uint256 i; i < length; i++) { + $globalSubHooks[msg.sender].push(hooks[i]); + } + } + + function installModuleHooks(address module, address[] memory hooks) public { + uint256 length = hooks.length; + for (uint256 i; i < length; i++) { + // check if the hook is already enabled for global + if ($globalSubHooks[msg.sender].contains(hooks[i])) continue; + $moduleSubHooks[msg.sender][module].push(hooks[i]); + } + } + + function _onExecSubHooks( + address sourceModule, + address target, + uint256 value, + bytes calldata callData + ) + internal + { + (address[] memory hooks,) = + $globalSubHooks[msg.sender].getEntriesPaginated(SENTINEL, MAX_HOOK_NR); + + for (uint256 i = 0; i < hooks.length; i++) { + SubHook(hooks[i]).onExecute(msg.sender, sourceModule, target, value, callData); + } + + (hooks,) = + $moduleSubHooks[msg.sender][sourceModule].getEntriesPaginated(SENTINEL, MAX_HOOK_NR); + for (uint256 i = 0; i < hooks.length; i++) { + SubHook(hooks[i]).onExecute(msg.sender, sourceModule, target, value, callData); + } + } +} diff --git a/examples/src/PermissionsHook/PermissionsHookV2.sol b/examples/src/PermissionsHook/PermissionsHookV2.sol new file mode 100644 index 00000000..fa471062 --- /dev/null +++ b/examples/src/PermissionsHook/PermissionsHookV2.sol @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { ERC7579HookDestruct } from "@rhinestone/modulekit/src/modules/ERC7579HookDestruct.sol"; +import { Execution, IERC7579Account } from "@rhinestone/modulekit/src/Accounts.sol"; +import { IERC7579Module } from "@rhinestone/modulekit/src/Modules.sol"; +import { SentinelListLib } from "sentinellist/SentinelList.sol"; +import { LinkedBytes32Lib } from "sentinellist/SentinelListBytes32.sol"; + +import { IERC721 } from "forge-std/interfaces/IERC721.sol"; +import { IERC20 } from "forge-std/interfaces/IERC20.sol"; + +import { HookMultiPlexer } from "./HookMultiplexer.sol"; +import { TransactionDetectionLib } from "./TransactionDetectionLib.sol"; + +import "forge-std/console2.sol"; + +contract PermissionsHook is ERC7579HookDestruct, HookMultiPlexer { + using SentinelListLib for SentinelListLib.SentinelList; + using LinkedBytes32Lib for LinkedBytes32Lib.LinkedBytes32; + using TransactionDetectionLib for bytes4; + /*////////////////////////////////////////////////////////////////////////// + CONSTANTS + //////////////////////////////////////////////////////////////////////////*/ + + error InvalidPermission(); + + struct InitParams { + AccessFlags flags; + address[] allowedTargets; + bytes32[] allowedFunctions; + address[] moduleSubHooks; + } + + struct AccessFlags { + // Execution permissions + // - Target permissions + bool selfCall; + bool moduleCall; + // - Value permissions + bool sendValue; + bool erc20Transfer; + bool erc721Transfer; + // - Calldata permissions + bool hasAllowedFunctions; + bool hasAllowedTargets; + // Module configuration permissions + bool moduleConfig; + } + + struct ModulePermissions { + AccessFlags flags; + LinkedBytes32Lib.LinkedBytes32 allowedFunctions; + SentinelListLib.SentinelList allowedTargets; + } + + mapping(address account => mapping(address module => ModulePermissions)) internal permissions; + + /*////////////////////////////////////////////////////////////////////////// + CONFIG + //////////////////////////////////////////////////////////////////////////*/ + + function onInstall(bytes calldata data) external override { + (address[] memory _modules, AccessFlags[] memory _initParams) = + abi.decode(data, (address[], AccessFlags[])); + + uint256 permissionsLength = _initParams.length; + + if (_modules.length != permissionsLength) { + revert("PermissionsHook: addPermissions: module and permissions length mismatch"); + } + + $globalSubHooks[msg.sender].init(); + + for (uint256 i; i < permissionsLength; i++) { + permissions[msg.sender][_modules[i]].flags = _initParams[i]; + $moduleSubHooks[msg.sender][_modules[i]].init(); + } + } + + function onUninstall(bytes calldata data) external override { + // todo + } + + function isInitialized(address smartAccount) external view returns (bool) { + // todo + } + + function addPermissions( + address[] calldata _modules, + AccessFlags[] calldata _accessFlags + ) + external + { + uint256 permissionsLength = _accessFlags.length; + + if (_modules.length != permissionsLength) { + revert("PermissionsHook: addPermissions: module and permissions length mismatch"); + } + + for (uint256 i; i < permissionsLength; i++) { + permissions[msg.sender][_modules[i]].flags = _accessFlags[i]; + } + } + + // function getPermissions( + // address account, + // address module + // ) + // public + // view + // returns (ModulePermissions memory) + // { + // return permissions[account][module]; + // } + + /*////////////////////////////////////////////////////////////////////////// + MODULE LOGIC + //////////////////////////////////////////////////////////////////////////*/ + + function onPostCheck(bytes calldata hookData) + internal + virtual + override + returns (bool success) + { + return true; + } + + function onExecute( + address sourceModule, + address target, + uint256 value, + bytes calldata callData + ) + internal + virtual + override + returns (bytes memory hookData) + { + // Not callable from module + ModulePermissions storage $permissions = permissions[msg.sender][sourceModule]; + console2.log("onExecute"); + _validateExecutePermissions($permissions, target, value, callData); + console2.log("validated Permisisons"); + + _onExecSubHooks(sourceModule, target, value, callData); + } + + function onExecuteBatch( + address msgSender, + Execution[] calldata + ) + internal + virtual + override + returns (bytes memory hookData) + { + // Not callable from module + } + + function onExecuteFromExecutor( + address msgSender, + address target, + uint256 value, + bytes calldata callData + ) + internal + virtual + override + returns (bytes memory hookData) + { + // ModulePermissions memory modulePermissions = permissions[msg.sender][msgSender]; + // _validateExecutePermissions(modulePermissions, target, value, callData); + } + + function onExecuteBatchFromExecutor( + address msgSender, + Execution[] calldata executions + ) + internal + virtual + override + returns (bytes memory hookData) + { + ModulePermissions storage modulePermissions = permissions[msg.sender][msgSender]; + + uint256 executionLength = executions.length; + for (uint256 i = 0; i < executionLength; i++) { + _validateExecutePermissions( + modulePermissions, executions[i].target, executions[i].value, executions[i].callData + ); + } + } + + function onInstallModule( + address msgSender, + uint256 moduleType, + address module, + bytes calldata initData + ) + internal + virtual + override + returns (bytes memory hookData) + { + bool isInstalledExecutor = + IERC7579Account(msg.sender).isModuleInstalled(TYPE_EXECUTOR, msgSender, ""); + + if (!isInstalledExecutor) { + // Execution not triggered by executor, so account should do access control + return ""; + } + + ModulePermissions storage $permissions = permissions[msg.sender][msgSender]; + + if (!$permissions.flags.moduleConfig) { + revert InvalidPermission(); + } + } + + function onUninstallModule( + address msgSender, + uint256 moduleType, + address module, + bytes calldata deInitData + ) + internal + virtual + override + returns (bytes memory hookData) + { + bool isInstalledExecutor = + IERC7579Account(msg.sender).isModuleInstalled(TYPE_EXECUTOR, msgSender, ""); + + if (!isInstalledExecutor) { + // Execution not triggered by executor, so account should do access control + return ""; + } + + ModulePermissions storage $permissions = permissions[msg.sender][msgSender]; + + if (!$permissions.flags.moduleConfig) { + revert InvalidPermission(); + } + } + + /*////////////////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////////////////*/ + + function _validateExecutePermissions( + ModulePermissions storage $permissions, + address target, + uint256 value, + bytes calldata callData + ) + internal + { + AccessFlags memory flags = $permissions.flags; + + bytes4 functionSig = callData.length > 4 ? bytes4(callData[0:4]) : bytes4(0); + console2.log("validate exeute permissions"); + + // check for self call + if (!flags.selfCall && target == msg.sender) { + revert InvalidPermission(); + } + + // check for module Call + if (!flags.moduleCall) { + // if (!flags.moduleCall && IERC7579Module(target).moduleId(msg.sender)) { + revert InvalidPermission(); + } + + // check for value transfer + if (!flags.sendValue && value > 0) { + revert InvalidPermission(); + } + + // Calldata permissions + if (!flags.erc20Transfer && functionSig.isERC20Transfer()) { + revert InvalidPermission(); + } + + if (!flags.erc721Transfer && functionSig.isERC721Transfer()) { + revert InvalidPermission(); + } + + // check if target address is allowed to be called + if (flags.hasAllowedTargets && !$permissions.allowedTargets.contains(target)) { + revert InvalidPermission(); + } + + // check if target functioni is allowed to be called + if ( + flags.hasAllowedFunctions + && !$permissions.allowedFunctions.contains(bytes32(functionSig)) + ) { + revert InvalidPermission(); + } + } + + /*////////////////////////////////////////////////////////////////////////// + METADATA + //////////////////////////////////////////////////////////////////////////*/ + + function version() external pure virtual returns (string memory) { + return "1.0.0"; + } + + function name() external pure virtual returns (string memory) { + return "PermissionHook"; + } + + function isModuleType(uint256 isType) external pure virtual override returns (bool) { + return isType == TYPE_HOOK; + } +} diff --git a/examples/src/PermissionsHook/TransactionDetectionLib.sol b/examples/src/PermissionsHook/TransactionDetectionLib.sol new file mode 100644 index 00000000..86d0bddc --- /dev/null +++ b/examples/src/PermissionsHook/TransactionDetectionLib.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { IERC721 } from "forge-std/interfaces/IERC721.sol"; +import { IERC20 } from "forge-std/interfaces/IERC20.sol"; + +library TransactionDetectionLib { + function isERC20Transfer(bytes4 functionSig) internal pure returns (bool isErc20Transfer) { + if (functionSig == IERC20.transfer.selector || functionSig == IERC20.transferFrom.selector) + { + isErc20Transfer = true; + } + } + + function isERC721Transfer(bytes4 functionSig) internal pure returns (bool isErc721Transfer) { + if (functionSig == IERC721.transferFrom.selector) { + isErc721Transfer = true; + } + } +} diff --git a/examples/src/PermissionsHook/subHooks/RegistryCheck.sol b/examples/src/PermissionsHook/subHooks/RegistryCheck.sol new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/src/PermissionsHook/subHooks/RegistryCheck.sol @@ -0,0 +1 @@ + diff --git a/examples/src/PermissionsHook/subHooks/SpendingLimit.sol b/examples/src/PermissionsHook/subHooks/SpendingLimit.sol new file mode 100644 index 00000000..e0795f05 --- /dev/null +++ b/examples/src/PermissionsHook/subHooks/SpendingLimit.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { SubHook } from "../HookMultiplexer.sol"; +import { Execution } from "@rhinestone/modulekit/src/Accounts.sol"; +import { TransactionDetectionLib } from "../TransactionDetectionLib.sol"; +import "forge-std/console2.sol"; + +contract SpendingLimit is SubHook { + using TransactionDetectionLib for bytes4; + + struct Limits { + uint256 totalSpent; + uint256 limit; + } + + mapping(address smartAccount => mapping(address token => Limits limit)) internal limit; + + constructor(address HookMultiplexer) SubHook(HookMultiplexer) { } + + function setLimit(address token, uint256 spendLimit) external { + limit[msg.sender][token].limit = spendLimit; + } + + function onExecute( + address smartAccount, + address module, + address target, + uint256 value, + bytes calldata callData + ) + external + override + onlyMultiPlexer + returns (bytes memory) + { + // only handle ERC20 transfers + if (callData.length < 4) return ""; + if (!bytes4(callData[0:4]).isERC20Transfer()) return ""; + + (address to, uint256 amount) = abi.decode(callData[4:], (address, uint256)); + + Limits storage $limit = limit[smartAccount][target]; + uint256 totalSpent = $limit.totalSpent + amount; + console2.log("totalSpent", totalSpent); + require(totalSpent <= $limit.limit, "SpendingLimit: limit exceeded"); + $limit.totalSpent = totalSpent; + } + + function onExecuteBatch( + address smartAccount, + address module, + Execution[] calldata executions + ) + external + override + onlyMultiPlexer + returns (bytes memory) + { } +} diff --git a/examples/test/PermissionsHook/PermissionsHook.t.sol b/examples/test/PermissionsHook/PermissionsHook.t.sol index af618aae..055beea6 100644 --- a/examples/test/PermissionsHook/PermissionsHook.t.sol +++ b/examples/test/PermissionsHook/PermissionsHook.t.sol @@ -3,17 +3,20 @@ pragma solidity ^0.8.23; import "forge-std/Test.sol"; import "@rhinestone/modulekit/src/ModuleKit.sol"; +import { IERC20 } from "forge-std/interfaces/IERC20.sol"; import { SessionData, SessionKeyManagerLib } from "@rhinestone/sessionkeymanager/src/SessionKeyManagerLib.sol"; -import { MockExecutor } from "@rhinestone/modulekit/src/Mocks.sol"; +import { MockExecutor, MockERC20 } from "@rhinestone/modulekit/src/Mocks.sol"; import { Solarray } from "solarray/Solarray.sol"; import { MODULE_TYPE_HOOK, MODULE_TYPE_EXECUTOR } from "@rhinestone/modulekit/src/external/ERC7579.sol"; -import { PermissionsHook, IERC7579Account } from "src/PermissionsHook/PermissionsHook.sol"; +import { PermissionsHook, IERC7579Account } from "src/PermissionsHook/PermissionsHookV2.sol"; + +import { SpendingLimit } from "src/PermissionsHook/subHooks/SpendingLimit.sol"; contract PermissionsHookTest is RhinestoneModuleKit, Test { using ModuleKitHelpers for *; @@ -21,12 +24,15 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { // Account instance and hook AccountInstance internal instance; + MockERC20 internal token; PermissionsHook internal permissionsHook; // Mock executors MockExecutor internal executorDisallowed; MockExecutor internal executorAllowed; + SpendingLimit internal spendingLimit; + address activeExecutor; bool activeCallSuccess; @@ -35,6 +41,9 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { permissionsHook = new PermissionsHook(); vm.label(address(permissionsHook), "permissionsHook"); + + spendingLimit = new SpendingLimit(address(permissionsHook)); + vm.label(address(spendingLimit), "SubHook:SpendingLimit"); executorDisallowed = new MockExecutor(); vm.label(address(executorDisallowed), "executorDisallowed"); executorAllowed = new MockExecutor(); @@ -43,6 +52,10 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { instance = makeAccountInstance("PermissionsHookTestAccount"); deal(address(instance.account), 100 ether); + token = new MockERC20(); + token.initialize("Mock Token", "MTK", 18); + deal(address(token), instance.account, 100 ether); + setUpPermissionsHook(); } @@ -64,9 +77,8 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { modules[1] = address(executorAllowed); modules[2] = address(instance.defaultValidator); - PermissionsHook.ModulePermissions[] memory permissions = - new PermissionsHook.ModulePermissions[](3); - permissions[0] = PermissionsHook.ModulePermissions({ + PermissionsHook.AccessFlags[] memory permissions = new PermissionsHook.AccessFlags[](3); + permissions[0] = PermissionsHook.AccessFlags({ selfCall: false, moduleCall: false, hasAllowedTargets: true, @@ -74,12 +86,10 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { hasAllowedFunctions: true, erc20Transfer: false, erc721Transfer: false, - moduleConfig: false, - allowedFunctions: new bytes4[](0), - allowedTargets: new address[](0) + moduleConfig: false }); - permissions[1] = PermissionsHook.ModulePermissions({ + permissions[1] = PermissionsHook.AccessFlags({ selfCall: true, moduleCall: true, hasAllowedTargets: false, @@ -87,12 +97,10 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { hasAllowedFunctions: false, erc20Transfer: true, erc721Transfer: true, - moduleConfig: true, - allowedFunctions: new bytes4[](0), - allowedTargets: new address[](0) + moduleConfig: true }); - permissions[2] = PermissionsHook.ModulePermissions({ + permissions[2] = PermissionsHook.AccessFlags({ selfCall: true, moduleCall: true, hasAllowedTargets: false, @@ -100,9 +108,7 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { hasAllowedFunctions: false, erc20Transfer: true, erc721Transfer: true, - moduleConfig: true, - allowedFunctions: new bytes4[](0), - allowedTargets: new address[](0) + moduleConfig: true }); console2.log("installing module"); @@ -112,6 +118,12 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { data: abi.encode(modules, permissions) }); console2.log("installed"); + + vm.prank(instance.account); + address[] memory subHooks = new address[](1); + subHooks[0] = address(spendingLimit); + + permissionsHook.installGlobalHooks(subHooks); } modifier performWithBothExecutors() { @@ -165,4 +177,18 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { instance.exec({ target: address(target), value: value, callData: callData }); assertEq(target.balance, balanceBefore + value); } + + function test_sendERC20() public { + address receiver = makeAddr("receiver"); + vm.prank(instance.account); + spendingLimit.setLimit(address(token), 200); + bytes memory callData = abi.encodeCall(IERC20.transfer, (receiver, 150)); + + instance.exec({ target: address(token), value: 0, callData: callData }); + + assertEq(token.balanceOf(receiver), 150); + + vm.expectRevert(); + instance.exec({ target: address(token), value: 0, callData: callData }); + } } diff --git a/packages/modulekit/foundry.toml b/packages/modulekit/foundry.toml index 7c06b254..a993bb0c 100644 --- a/packages/modulekit/foundry.toml +++ b/packages/modulekit/foundry.toml @@ -5,6 +5,8 @@ libs = ["node_modules"] #via_ir = true fs_permissions = [{ access = "read", path = "out-optimized" }, { access = "read-write", path = "gas_calculations" }] allow_paths = ["*", "/"] +evm_version = "cancun" +ignored_error_codes = [2394] [rpc_endpoints] mainnet = "https://rpc.ankr.com/eth" diff --git a/packages/modulekit/src/Modules.sol b/packages/modulekit/src/Modules.sol index 5222b7a3..fe42694e 100644 --- a/packages/modulekit/src/Modules.sol +++ b/packages/modulekit/src/Modules.sol @@ -13,5 +13,6 @@ import { IERC7579Validator, IERC7579Executor, IERC7579Fallback, - IERC7579Hook + IERC7579Hook, + IERC7579Module } from "./external/ERC7579.sol"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c35bdd3e..d7de6ea5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -109,10 +109,10 @@ importers: version: github.com/dapphub/ds-test/e282159d5170298eb2455a6c05280ab5a73a4ef0 erc4337-validation: specifier: github:rhinestonewtf/erc4337-validation - version: git@github.com+rhinestonewtf/erc4337-validation/19a97d86f8f29709664334078925b2a843be19e0 + version: github.com/rhinestonewtf/erc4337-validation/19a97d86f8f29709664334078925b2a843be19e0 erc7579: specifier: github:erc7579/erc7579-implementation#feature/hookRevert - version: github.com/erc7579/erc7579-implementation/cb007568e1c420ddbddf83f1880a4dc7c981e178 + version: github.com/erc7579/erc7579-implementation/240e114638eaa10facc01cd8cb6d7a32e1c63269 forge-std: specifier: github:foundry-rs/forge-std version: github.com/foundry-rs/forge-std/1d0766bc5d814f117c7b1e643828f7d85024fb51 @@ -121,7 +121,7 @@ importers: version: 2.8.8 sentinellist: specifier: github:zeroknots/sentinellist - version: github.com/zeroknots/sentinellist/6294bf412489c0f6d3b9c92ad0aceb08c5b8704b + version: github.com/zeroknots/sentinellist/5f851f29b5d5e0fd4f5cdc63a2ccd7865ab1802d solady: specifier: github:vectorized/solady version: github.com/vectorized/solady/21009ce09f02c0e20ce4750b63577e8c0cc7ced8 @@ -4456,31 +4456,22 @@ packages: ethers: 5.7.2 dev: true - git@github.com+rhinestonewtf/erc4337-validation/19a97d86f8f29709664334078925b2a843be19e0: - resolution: {commit: 19a97d86f8f29709664334078925b2a843be19e0, repo: git@github.com:rhinestonewtf/erc4337-validation.git, type: git} - name: erc4337-validation - version: 0.0.1 - dependencies: - '@openzeppelin/contracts': 5.0.1 - solady: github.com/vectorized/solady/21009ce09f02c0e20ce4750b63577e8c0cc7ced8 - dev: true - github.com/dapphub/ds-test/e282159d5170298eb2455a6c05280ab5a73a4ef0: resolution: {tarball: https://codeload.github.com/dapphub/ds-test/tar.gz/e282159d5170298eb2455a6c05280ab5a73a4ef0} name: ds-test version: 1.0.0 dev: true - github.com/erc7579/erc7579-implementation/8bdabebe50afeaed5d3c7916bfb98a5290de1dc3: - resolution: {tarball: https://codeload.github.com/erc7579/erc7579-implementation/tar.gz/8bdabebe50afeaed5d3c7916bfb98a5290de1dc3} + github.com/erc7579/erc7579-implementation/240e114638eaa10facc01cd8cb6d7a32e1c63269: + resolution: {tarball: https://codeload.github.com/erc7579/erc7579-implementation/tar.gz/240e114638eaa10facc01cd8cb6d7a32e1c63269} name: micro-msa version: 0.3.1 dependencies: '@openzeppelin/contracts': 5.0.1 dev: true - github.com/erc7579/erc7579-implementation/cb007568e1c420ddbddf83f1880a4dc7c981e178: - resolution: {tarball: https://codeload.github.com/erc7579/erc7579-implementation/tar.gz/cb007568e1c420ddbddf83f1880a4dc7c981e178} + github.com/erc7579/erc7579-implementation/8bdabebe50afeaed5d3c7916bfb98a5290de1dc3: + resolution: {tarball: https://codeload.github.com/erc7579/erc7579-implementation/tar.gz/8bdabebe50afeaed5d3c7916bfb98a5290de1dc3} name: micro-msa version: 0.3.1 dependencies: @@ -4692,6 +4683,12 @@ packages: version: 0.0.170 dev: true + github.com/zeroknots/sentinellist/5f851f29b5d5e0fd4f5cdc63a2ccd7865ab1802d: + resolution: {tarball: https://codeload.github.com/zeroknots/sentinellist/tar.gz/5f851f29b5d5e0fd4f5cdc63a2ccd7865ab1802d} + name: sentinellist + version: 1.0.0 + dev: true + github.com/zeroknots/sentinellist/6294bf412489c0f6d3b9c92ad0aceb08c5b8704b: resolution: {tarball: https://codeload.github.com/zeroknots/sentinellist/tar.gz/6294bf412489c0f6d3b9c92ad0aceb08c5b8704b} name: sentinellist From d16bab31390531b0f55dfb0ceafdd999be2d2cd4 Mon Sep 17 00:00:00 2001 From: zeroknots Date: Tue, 27 Feb 2024 17:39:29 +0700 Subject: [PATCH 6/9] feat: multiplex --- examples/src/HookMultiplex/Base.sol | 224 ++++++++++++++++++ .../src/HookMultiplex/HookMultiplexer.sol | 163 +++++++++++++ examples/src/HookMultiplex/ISubHook.sol | 66 ++++++ .../HookMultiplex/lib/TokenTransactionLib.sol | 20 ++ .../subHooks/PermissionFlags.sol | 197 +++++++++++++++ .../HookMultiplex/subHooks/SpendingLimits.sol | 112 +++++++++ .../src/PermissionsHook/HookMultiplexer.sol | 1 + .../subHooks/RegistryCheck.sol | 1 - .../subHooks/SpendingLimit.sol | 60 ----- .../HookMultiPlexer/HookMultiplexer.t.sol | 120 ++++++++++ ...nsHook.t.sol => PermissionsHook.t.sol.bak} | 7 +- 11 files changed, 904 insertions(+), 67 deletions(-) create mode 100644 examples/src/HookMultiplex/Base.sol create mode 100644 examples/src/HookMultiplex/HookMultiplexer.sol create mode 100644 examples/src/HookMultiplex/ISubHook.sol create mode 100644 examples/src/HookMultiplex/lib/TokenTransactionLib.sol create mode 100644 examples/src/HookMultiplex/subHooks/PermissionFlags.sol create mode 100644 examples/src/HookMultiplex/subHooks/SpendingLimits.sol delete mode 100644 examples/src/PermissionsHook/subHooks/RegistryCheck.sol delete mode 100644 examples/src/PermissionsHook/subHooks/SpendingLimit.sol create mode 100644 examples/test/HookMultiPlexer/HookMultiplexer.t.sol rename examples/test/PermissionsHook/{PermissionsHook.t.sol => PermissionsHook.t.sol.bak} (95%) diff --git a/examples/src/HookMultiplex/Base.sol b/examples/src/HookMultiplex/Base.sol new file mode 100644 index 00000000..3c1da0f2 --- /dev/null +++ b/examples/src/HookMultiplex/Base.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { ERC7579HookBase } from "@rhinestone/modulekit/src/modules/ERC7579HookBase.sol"; +import { IERC7579Account } from "@rhinestone/modulekit/src/Accounts.sol"; +import { ExecutionLib, Execution } from "erc7579/lib/ExecutionLib.sol"; +import { + ModeLib, + CallType, + ModeCode, + CALLTYPE_SINGLE, + CALLTYPE_BATCH, + CALLTYPE_DELEGATECALL +} from "erc7579/lib/ModeLib.sol"; + +uint256 constant EXEC_OFFSET = 100; +uint256 constant INSTALL_OFFSET = 132; + +abstract contract ERC7579HookDestruct is ERC7579HookBase { + error HookInvalidSelector(); + + /*////////////////////////////////////////////////////////////////////////// + CALLDATA DECODING + //////////////////////////////////////////////////////////////////////////*/ + + function preCheck( + address msgSender, + bytes calldata msgData + ) + external + virtual + override + returns (bytes memory hookData) + { + bytes4 selector = bytes4(msgData[0:4]); + + if (selector == IERC7579Account.execute.selector) { + return _handle4337Executions(msgSender, msgData); + } else if (selector == IERC7579Account.executeFromExecutor.selector) { + return _handleExecutorExecutions(msgSender, msgData); + } else if (selector == IERC7579Account.installModule.selector) { + uint256 paramLen = uint256(bytes32(msgData[INSTALL_OFFSET - 32:INSTALL_OFFSET])); + bytes calldata initData = msgData[INSTALL_OFFSET:INSTALL_OFFSET + paramLen]; + uint256 moduleType = uint256(bytes32(msgData[4:36])); + address module = address(bytes20((msgData[48:68]))); + return onInstallModule(msgSender, moduleType, module, initData); + } else if (selector == IERC7579Account.uninstallModule.selector) { + uint256 paramLen = uint256(bytes32(msgData[INSTALL_OFFSET - 32:INSTALL_OFFSET])); + bytes calldata initData = msgData[INSTALL_OFFSET:INSTALL_OFFSET + paramLen]; + uint256 moduleType = uint256(bytes32(msgData[4:36])); + address module = address(bytes20((msgData[48:68]))); + + return onUninstallModule(msgSender, moduleType, module, initData); + } else { + revert(); + } + } + + function _handle4337Executions( + address msgSender, + bytes calldata msgData + ) + internal + returns (bytes memory hookData) + { + uint256 paramLen = uint256(bytes32(msgData[EXEC_OFFSET - 32:EXEC_OFFSET])); + bytes calldata encodedExecutions = msgData[EXEC_OFFSET:EXEC_OFFSET + paramLen]; + + ModeCode mode = ModeCode.wrap(bytes32(msgData[4:36])); + CallType calltype = ModeLib.getCallType(mode); + + if (calltype == CALLTYPE_SINGLE) { + (address to, uint256 value, bytes calldata callData) = + ExecutionLib.decodeSingle(encodedExecutions); + return onExecute(msgSender, to, value, callData); + } else if (calltype == CALLTYPE_BATCH) { + Execution[] calldata execs = ExecutionLib.decodeBatch(encodedExecutions); + return onExecuteBatch(msgSender, execs); + } + } + + function _handleExecutorExecutions( + address msgSender, + bytes calldata msgData + ) + internal + returns (bytes memory hookData) + { + uint256 paramLen = uint256(bytes32(msgData[EXEC_OFFSET - 32:EXEC_OFFSET])); + bytes calldata encodedExecutions = msgData[EXEC_OFFSET:EXEC_OFFSET + paramLen]; + + ModeCode mode = ModeCode.wrap(bytes32(msgData[4:36])); + CallType calltype = ModeLib.getCallType(mode); + + if (calltype == CALLTYPE_SINGLE) { + (address to, uint256 value, bytes calldata callData) = + ExecutionLib.decodeSingle(encodedExecutions); + return onExecuteFromExecutor(msgSender, to, value, callData); + } else if (calltype == CALLTYPE_BATCH) { + Execution[] calldata execs = ExecutionLib.decodeBatch(encodedExecutions); + return onExecuteBatchFromExecutor(msgSender, execs); + } + } + + // if (selector == IERC7579Account.execute.selector) { + // ModeCode mode = ModeCode.wrap(bytes32(msgData[4:36])); + // CallType calltype = ModeLib.getCallType(mode); + // uint256 offset = msgData.offset(); + // if (calltype == CALLTYPE_SINGLE) { + // (address to, uint256 value, bytes calldata callData) = + // ExecutionLib.decodeSingle(msgData[36:offset]); + // return onExecute(msgSender, to, value, callData); + // } else if (calltype == CALLTYPE_BATCH) { + // Execution[] calldata execs = ExecutionLib.decodeBatch(msgData[36:offset]); + // return onExecuteBatch(msgSender, execs); + // } else { + // revert HookInvalidSelector(); + // } + // } else if (selector == IERC7579Account.executeFromExecutor.selector) { + // uint256 offset = msgData.offset(); + // + // ModeCode mode = ModeCode.wrap(bytes32(msgData[4:36])); + // CallType calltype = ModeLib.getCallType(mode); + // if (calltype == CALLTYPE_SINGLE) { + // (address to, uint256 value, bytes calldata callData) = + // ExecutionLib.decodeSingle(msgData[36:offset]); + // return onExecuteFromExecutor(msgSender, to, value, callData); + // } else if (calltype == CALLTYPE_BATCH) { + // Execution[] calldata execs = ExecutionLib.decodeBatch(msgData[36:offset]); + // return onExecuteBatchFromExecutor(msgSender, execs); + // } else { + // revert HookInvalidSelector(); + // } + // } else if (selector == IERC7579Account.installModule.selector) { + // uint256 offset = msgData.offset(); + // uint256 moduleType = uint256(bytes32(msgData[4:24])); + // address module = address(bytes20(msgData[24:36])); + // bytes calldata initData = msgData[36:offset]; + // onInstallModule(msgSender, moduleType, module, initData); + // } else if (selector == IERC7579Account.uninstallModule.selector) { + // uint256 offset = msgData.offset(); + // uint256 moduleType = uint256(bytes32(msgData[4:24])); + // address module = address(bytes20(msgData[24:36])); + // bytes calldata initData = msgData[36:offset]; + // onUninstallModule(msgSender, moduleType, module, initData); + // } else { + // revert HookInvalidSelector(); + // } + + function postCheck(bytes calldata hookData) external override { + if (hookData.length == 0) return; + if (onPostCheck(hookData) == false) revert(); + } + + /*////////////////////////////////////////////////////////////////////////// + EXECUTION + //////////////////////////////////////////////////////////////////////////*/ + + function onExecute( + address msgSender, + address target, + uint256 value, + bytes calldata callData + ) + internal + virtual + returns (bytes memory hookData); + + function onExecuteBatch( + address msgSender, + Execution[] calldata + ) + internal + virtual + returns (bytes memory hookData); + + function onExecuteFromExecutor( + address msgSender, + address target, + uint256 value, + bytes calldata callData + ) + internal + virtual + returns (bytes memory hookData); + + function onExecuteBatchFromExecutor( + address msgSender, + Execution[] calldata + ) + internal + virtual + returns (bytes memory hookData); + + /*////////////////////////////////////////////////////////////////////////// + CONFIG + //////////////////////////////////////////////////////////////////////////*/ + + function onInstallModule( + address msgSender, + uint256 moduleType, + address module, + bytes calldata initData + ) + internal + virtual + returns (bytes memory hookData); + + function onUninstallModule( + address msgSender, + uint256 moduleType, + address module, + bytes calldata deInitData + ) + internal + virtual + returns (bytes memory hookData); + + /*////////////////////////////////////////////////////////////////////////// + POSTCHECK + //////////////////////////////////////////////////////////////////////////*/ + + function onPostCheck(bytes calldata hookData) internal virtual returns (bool success); +} diff --git a/examples/src/HookMultiplex/HookMultiplexer.sol b/examples/src/HookMultiplex/HookMultiplexer.sol new file mode 100644 index 00000000..f2699f86 --- /dev/null +++ b/examples/src/HookMultiplex/HookMultiplexer.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { Execution } from "@rhinestone/modulekit/src/Accounts.sol"; +import { SENTINEL, SentinelListLib } from "sentinellist/SentinelList.sol"; +import { ERC7579HookDestruct } from "@rhinestone/modulekit/src/modules/ERC7579HookDestruct.sol"; +import "forge-std/console2.sol"; +import { ISubHook } from "./ISubHook.sol"; + +contract HookMultiPlexer is ERC7579HookDestruct { + using SentinelListLib for SentinelListLib.SentinelList; + + 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; + for (uint256 i; i < length; i++) { + $globalSubHooks[msg.sender].push(hooks[i]); + // TODO check if the hook is already enabled for module + } + } + + function installModuleHooks(address module, address[] memory hooks) public { + uint256 length = hooks.length; + for (uint256 i; i < length; i++) { + // check if the hook is already enabled for global + if ($globalSubHooks[msg.sender].contains(hooks[i])) continue; + $moduleSubHooks[msg.sender][module].push(hooks[i]); + } + } + + function onInstall(bytes calldata data) external override { + $globalSubHooks[msg.sender].init(); + } + + function onUninstall(bytes calldata data) external override { + // todo + } + + function version() external pure virtual returns (string memory) { + return "1.0.0"; + } + + function name() external pure virtual returns (string memory) { + return "PermissionHook"; + } + + function isModuleType(uint256 isType) external pure virtual override returns (bool) { + return isType == TYPE_HOOK; + } + + function isInitialized(address smartAccount) external view returns (bool) { + // todo + } + + function _delegatecallSubHook( + bytes4 functionSig, + address subHook, + address msgSender, + address target, + uint256 value, + bytes calldata callData + ) + internal + { } + + function onExecute( + address msgSender, + address target, + uint256 value, + bytes calldata callData + ) + internal + virtual + 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 + } + + function onExecuteBatch( + address msgSender, + Execution[] calldata + ) + internal + virtual + override + returns (bytes memory hookData) + { } + + function onExecuteFromExecutor( + address msgSender, + address target, + uint256 value, + bytes calldata callData + ) + internal + virtual + override + returns (bytes memory hookData) + { } + + function onExecuteBatchFromExecutor( + address msgSender, + Execution[] calldata + ) + internal + virtual + override + returns (bytes memory hookData) + { } + + function onInstallModule( + address msgSender, + uint256 moduleType, + address module, + bytes calldata initData + ) + internal + virtual + override + returns (bytes memory hookData) + { } + + function onUninstallModule( + address msgSender, + uint256 moduleType, + address module, + bytes calldata deInitData + ) + internal + virtual + override + returns (bytes memory hookData) + { } + + function onPostCheck(bytes calldata hookData) + internal + virtual + override + returns (bool success) + { } +} diff --git a/examples/src/HookMultiplex/ISubHook.sol b/examples/src/HookMultiplex/ISubHook.sol new file mode 100644 index 00000000..ee2810b9 --- /dev/null +++ b/examples/src/HookMultiplex/ISubHook.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { Execution } from "@rhinestone/modulekit/src/Accounts.sol"; + +interface ISubHook { + function onExecute( + address msgSender, + address target, + uint256 value, + bytes calldata callData + ) + external + returns (bytes memory hookData); + + function onExecuteBatch( + address msgSender, + Execution[] calldata + ) + external + returns (bytes memory hookData); + + function onExecuteFromExecutor( + address msgSender, + address target, + uint256 value, + bytes calldata callData + ) + external + returns (bytes memory hookData); + + function onExecuteBatchFromExecutor( + address msgSender, + Execution[] calldata + ) + external + returns (bytes memory hookData); + + /*////////////////////////////////////////////////////////////////////////// + CONFIG + //////////////////////////////////////////////////////////////////////////*/ + + function onInstallModule( + address msgSender, + uint256 moduleType, + address module, + bytes calldata initData + ) + external + returns (bytes memory hookData); + + function onUninstallModule( + address msgSender, + uint256 moduleType, + address module, + bytes calldata deInitData + ) + external + returns (bytes memory hookData); + + /*////////////////////////////////////////////////////////////////////////// + POSTCHECK + //////////////////////////////////////////////////////////////////////////*/ + + function onPostCheck(bytes calldata hookData) external returns (bool success); +} diff --git a/examples/src/HookMultiplex/lib/TokenTransactionLib.sol b/examples/src/HookMultiplex/lib/TokenTransactionLib.sol new file mode 100644 index 00000000..cfedd1fc --- /dev/null +++ b/examples/src/HookMultiplex/lib/TokenTransactionLib.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { IERC721 } from "forge-std/interfaces/IERC721.sol"; +import { IERC20 } from "forge-std/interfaces/IERC20.sol"; + +library TokenTransactionLib { + function isERC20Transfer(bytes4 functionSig) internal pure returns (bool isErc20Transfer) { + if (functionSig == IERC20.transfer.selector || functionSig == IERC20.transferFrom.selector) + { + isErc20Transfer = true; + } + } + + function isERC721Transfer(bytes4 functionSig) internal pure returns (bool isErc721Transfer) { + if (functionSig == IERC721.transferFrom.selector) { + isErc721Transfer = true; + } + } +} diff --git a/examples/src/HookMultiplex/subHooks/PermissionFlags.sol b/examples/src/HookMultiplex/subHooks/PermissionFlags.sol new file mode 100644 index 00000000..14eef649 --- /dev/null +++ b/examples/src/HookMultiplex/subHooks/PermissionFlags.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: MIT +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 { 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 { + using SentinelListLib for SentinelListLib.SentinelList; + using LinkedBytes32Lib for LinkedBytes32Lib.LinkedBytes32; + using TokenTransactionLib for bytes4; + + error InvalidPermission(); + + struct AccessFlags { + // Execution permissions + // - Target permissions + bool selfCall; + bool moduleCall; + // - Value permissions + bool sendValue; + bool erc20Transfer; + bool erc721Transfer; + // - Calldata permissions + bool hasAllowedFunctions; + bool hasAllowedTargets; + // Module configuration permissions + bool moduleConfig; + } + + struct ModulePermissions { + AccessFlags flags; + LinkedBytes32Lib.LinkedBytes32 allowedFunctions; + SentinelListLib.SentinelList allowedTargets; + } + + struct SubHookStorage { + mapping(address account => mapping(address module => ModulePermissions)) permissions; + } + + function $subHook() internal pure virtual returns (SubHookStorage storage shs) { + bytes32 position = STORAGE_SLOT; + assembly { + shs.slot := position + } + } + + function configure( + address module, + AccessFlags calldata flags, + address[] calldata allowedTargets, + bytes4[] calldata allowedFunctions + ) + external + { + ModulePermissions storage $modulePermissions = $subHook().permissions[msg.sender][module]; + $modulePermissions.flags = flags; + + uint256 length = allowedTargets.length; + $modulePermissions.allowedTargets.init(); + for (uint256 i; i < length; i++) { + $modulePermissions.allowedTargets.push(allowedTargets[i]); + } + length = allowedFunctions.length; + for (uint256 i; i < length; i++) { + $modulePermissions.allowedFunctions.push(bytes32(allowedFunctions[i])); + } + } + + function onExecute( + address superVisorModule, + address target, + uint256 value, + bytes calldata callData + ) + external + virtual + override + returns (bytes memory hookData) + { + console2.log("onExecute subhook"); + ModulePermissions storage $modulePermissions = + $subHook().permissions[msg.sender][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) { + revert InvalidPermission(); + } + + // check for module Call + // TODO: + // if (!flags.moduleCall) { + // // if (!flags.moduleCall && IERC7579Module(target).moduleId(msg.sender)) { + // revert InvalidPermission(); + // } + + // check for value transfer + if (!flags.sendValue && value > 0) { + revert InvalidPermission(); + } + + // Calldata permissions + if (flags.erc20Transfer && functionSig.isERC20Transfer()) { + revert InvalidPermission(); + } + + if (flags.erc721Transfer && functionSig.isERC721Transfer()) { + revert InvalidPermission(); + } + + // check if target address is allowed to be called + if (flags.hasAllowedTargets && !$modulePermissions.allowedTargets.contains(target)) { + revert InvalidPermission(); + } + + // check if target functioni is allowed to be called + if ( + flags.hasAllowedFunctions + && !$modulePermissions.allowedFunctions.contains(bytes32(functionSig)) + ) { + revert InvalidPermission(); + } + } + + function onExecuteBatch( + address superVisorModule, + Execution[] calldata + ) + external + virtual + override + returns (bytes memory hookData) + { } + + function onExecuteFromExecutor( + address superVisorModule, + address target, + uint256 value, + bytes calldata callData + ) + external + virtual + override + returns (bytes memory hookData) + { } + + function onExecuteBatchFromExecutor( + address superVisorModule, + Execution[] calldata + ) + external + virtual + override + returns (bytes memory hookData) + { } + + function onInstallModule( + address superVisorModule, + uint256 moduleType, + address module, + bytes calldata initData + ) + external + virtual + override + returns (bytes memory hookData) + { } + + function onUninstallModule( + address superVisorModule, + uint256 moduleType, + address module, + bytes calldata deInitData + ) + external + virtual + override + returns (bytes memory hookData) + { } + + function onPostCheck(bytes calldata hookData) + external + virtual + override + returns (bool success) + { } +} diff --git a/examples/src/HookMultiplex/subHooks/SpendingLimits.sol b/examples/src/HookMultiplex/subHooks/SpendingLimits.sol new file mode 100644 index 00000000..96ba00cd --- /dev/null +++ b/examples/src/HookMultiplex/subHooks/SpendingLimits.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +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 { 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 { + using TokenTransactionLib for bytes4; + + struct Limits { + uint256 totalSpent; + uint256 limit; + } + + struct SubHookStorage { + mapping(address smartAccount => mapping(address token => Limits limit)) limit; + } + + function $subHook() internal pure virtual returns (SubHookStorage storage shs) { + bytes32 position = STORAGE_SLOT; + assembly { + shs.slot := position + } + } + + function configure(address token, uint256 spendLimit) external { + Limits storage $limit = $subHook().limit[msg.sender][token]; + $limit.limit = spendLimit; + } + + function onExecute( + address msgSender, + address target, + uint256 value, + bytes calldata callData + ) + external + override + returns (bytes memory hookData) + { + if (callData.length < 4) return ""; + if (!bytes4(callData[0:4]).isERC20Transfer()) return ""; + + (address to, uint256 amount) = abi.decode(callData[4:], (address, uint256)); + + Limits storage $limit = $subHook().limit[msg.sender][target]; + uint256 totalSpent = $limit.totalSpent + amount; + console2.log("totalSpent", totalSpent); + require(totalSpent <= $limit.limit, "SpendingLimit: limit exceeded"); + $limit.totalSpent = totalSpent; + } + + function onExecuteBatch( + address msgSender, + Execution[] calldata + ) + external + override + returns (bytes memory hookData) + { } + + function onExecuteFromExecutor( + address msgSender, + address target, + uint256 value, + bytes calldata callData + ) + external + override + returns (bytes memory hookData) + { } + + function onExecuteBatchFromExecutor( + address msgSender, + Execution[] calldata + ) + external + override + returns (bytes memory hookData) + { } + + function onInstallModule( + address msgSender, + uint256 moduleType, + address module, + bytes calldata initData + ) + external + override + returns (bytes memory hookData) + { } + + function onUninstallModule( + address msgSender, + uint256 moduleType, + address module, + bytes calldata deInitData + ) + external + override + returns (bytes memory hookData) + { } + + function onPostCheck(bytes calldata hookData) external override returns (bool success) { } +} diff --git a/examples/src/PermissionsHook/HookMultiplexer.sol b/examples/src/PermissionsHook/HookMultiplexer.sol index b6c0f5f7..399a0ee4 100644 --- a/examples/src/PermissionsHook/HookMultiplexer.sol +++ b/examples/src/PermissionsHook/HookMultiplexer.sol @@ -53,6 +53,7 @@ abstract contract HookMultiPlexer { uint256 length = hooks.length; for (uint256 i; i < length; i++) { $globalSubHooks[msg.sender].push(hooks[i]); + // TODO check if the hook is already enabled for module } } diff --git a/examples/src/PermissionsHook/subHooks/RegistryCheck.sol b/examples/src/PermissionsHook/subHooks/RegistryCheck.sol deleted file mode 100644 index 8b137891..00000000 --- a/examples/src/PermissionsHook/subHooks/RegistryCheck.sol +++ /dev/null @@ -1 +0,0 @@ - diff --git a/examples/src/PermissionsHook/subHooks/SpendingLimit.sol b/examples/src/PermissionsHook/subHooks/SpendingLimit.sol deleted file mode 100644 index e0795f05..00000000 --- a/examples/src/PermissionsHook/subHooks/SpendingLimit.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import { SubHook } from "../HookMultiplexer.sol"; -import { Execution } from "@rhinestone/modulekit/src/Accounts.sol"; -import { TransactionDetectionLib } from "../TransactionDetectionLib.sol"; -import "forge-std/console2.sol"; - -contract SpendingLimit is SubHook { - using TransactionDetectionLib for bytes4; - - struct Limits { - uint256 totalSpent; - uint256 limit; - } - - mapping(address smartAccount => mapping(address token => Limits limit)) internal limit; - - constructor(address HookMultiplexer) SubHook(HookMultiplexer) { } - - function setLimit(address token, uint256 spendLimit) external { - limit[msg.sender][token].limit = spendLimit; - } - - function onExecute( - address smartAccount, - address module, - address target, - uint256 value, - bytes calldata callData - ) - external - override - onlyMultiPlexer - returns (bytes memory) - { - // only handle ERC20 transfers - if (callData.length < 4) return ""; - if (!bytes4(callData[0:4]).isERC20Transfer()) return ""; - - (address to, uint256 amount) = abi.decode(callData[4:], (address, uint256)); - - Limits storage $limit = limit[smartAccount][target]; - uint256 totalSpent = $limit.totalSpent + amount; - console2.log("totalSpent", totalSpent); - require(totalSpent <= $limit.limit, "SpendingLimit: limit exceeded"); - $limit.totalSpent = totalSpent; - } - - function onExecuteBatch( - address smartAccount, - address module, - Execution[] calldata executions - ) - external - override - onlyMultiPlexer - returns (bytes memory) - { } -} diff --git a/examples/test/HookMultiPlexer/HookMultiplexer.t.sol b/examples/test/HookMultiPlexer/HookMultiplexer.t.sol new file mode 100644 index 00000000..e26ea0b9 --- /dev/null +++ b/examples/test/HookMultiPlexer/HookMultiplexer.t.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "forge-std/Test.sol"; +import "@rhinestone/modulekit/src/ModuleKit.sol"; +import { IERC20 } from "forge-std/interfaces/IERC20.sol"; +import { + SessionData, + SessionKeyManagerLib +} from "@rhinestone/sessionkeymanager/src/SessionKeyManagerLib.sol"; +import { MockExecutor, MockERC20 } from "@rhinestone/modulekit/src/Mocks.sol"; +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 { PermissionFlags } from "src/HookMultiPlex/subHooks/PermissionFlags.sol"; +import { SpendingLimits } from "src/HookMultiPlex/subHooks/SpendingLimits.sol"; + +contract HookMultiPlexerTest is RhinestoneModuleKit, Test { + using ModuleKitHelpers for *; + using ModuleKitUserOp for *; + + // Account instance and hook + AccountInstance internal instance; + MockERC20 internal token; + HookMultiPlexer internal multiplexer; + PermissionFlags internal permissionFlagsSubHook; + SpendingLimits internal spendingLimitsSubHook; + + // Mock executors + MockExecutor internal executorDisallowed; + MockExecutor internal executorAllowed; + + address activeExecutor; + bool activeCallSuccess; + + function setUp() public { + init(); + + multiplexer = new HookMultiPlexer(); + vm.label(address(multiplexer), "multiplexer"); + + permissionFlagsSubHook = new PermissionFlags(); + vm.label(address(permissionFlagsSubHook), "SubHook:PermissionFlags"); + spendingLimitsSubHook = new SpendingLimits(); + vm.label(address(spendingLimitsSubHook), "SubHook:SpendingLimits"); + + executorDisallowed = new MockExecutor(); + vm.label(address(executorDisallowed), "executorDisallowed"); + executorAllowed = new MockExecutor(); + vm.label(address(executorAllowed), "executorAllowed"); + + instance = makeAccountInstance("PermissionsHookTestAccount"); + deal(address(instance.account), 100 ether); + + token = new MockERC20(); + token.initialize("Mock Token", "MTK", 18); + deal(address(token), instance.account, 100 ether); + + setupMultiplexer(); + setUpPermissionsSubHook(); + setupSpendingLimitsSubHook(); + } + + function setupMultiplexer() internal { + instance.installModule({ + moduleTypeId: MODULE_TYPE_HOOK, + module: address(multiplexer), + data: "" + }); + + vm.prank(instance.account); + address[] memory globalHooks = new address[](2); + globalHooks[0] = address(permissionFlagsSubHook); + globalHooks[1] = address(spendingLimitsSubHook); + multiplexer.installGlobalHooks(globalHooks); + } + + function setUpPermissionsSubHook() internal { + PermissionFlags.AccessFlags memory flags = PermissionFlags.AccessFlags({ + selfCall: false, + moduleCall: false, + hasAllowedTargets: true, + sendValue: false, + hasAllowedFunctions: true, + erc20Transfer: true, + erc721Transfer: false, + moduleConfig: false + }); + + vm.prank(instance.account); + permissionFlagsSubHook.configure({ + module: address(instance.defaultValidator), + flags: flags, + allowedTargets: new address[](0), + allowedFunctions: new bytes4[](0) + }); + } + + function setupSpendingLimitsSubHook() public { + vm.prank(instance.account); + spendingLimitsSubHook.configure(address(token), 500); + } + + function test_sendERC20() public { + address receiver = makeAddr("receiver"); + vm.prank(instance.account); + bytes memory callData = abi.encodeCall(IERC20.transfer, (receiver, 150)); + + instance.exec({ target: address(token), value: 0, callData: callData }); + + assertEq(token.balanceOf(receiver), 150); + callData = abi.encodeCall(IERC20.transfer, (receiver, 500)); + vm.expectRevert(); + instance.exec({ target: address(token), value: 0, callData: callData }); + } +} diff --git a/examples/test/PermissionsHook/PermissionsHook.t.sol b/examples/test/PermissionsHook/PermissionsHook.t.sol.bak similarity index 95% rename from examples/test/PermissionsHook/PermissionsHook.t.sol rename to examples/test/PermissionsHook/PermissionsHook.t.sol.bak index 055beea6..ff278f93 100644 --- a/examples/test/PermissionsHook/PermissionsHook.t.sol +++ b/examples/test/PermissionsHook/PermissionsHook.t.sol.bak @@ -16,8 +16,6 @@ import { } from "@rhinestone/modulekit/src/external/ERC7579.sol"; import { PermissionsHook, IERC7579Account } from "src/PermissionsHook/PermissionsHookV2.sol"; -import { SpendingLimit } from "src/PermissionsHook/subHooks/SpendingLimit.sol"; - contract PermissionsHookTest is RhinestoneModuleKit, Test { using ModuleKitHelpers for *; using ModuleKitUserOp for *; @@ -31,7 +29,6 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { MockExecutor internal executorDisallowed; MockExecutor internal executorAllowed; - SpendingLimit internal spendingLimit; address activeExecutor; bool activeCallSuccess; @@ -42,8 +39,6 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { permissionsHook = new PermissionsHook(); vm.label(address(permissionsHook), "permissionsHook"); - spendingLimit = new SpendingLimit(address(permissionsHook)); - vm.label(address(spendingLimit), "SubHook:SpendingLimit"); executorDisallowed = new MockExecutor(); vm.label(address(executorDisallowed), "executorDisallowed"); executorAllowed = new MockExecutor(); @@ -121,7 +116,7 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { vm.prank(instance.account); address[] memory subHooks = new address[](1); - subHooks[0] = address(spendingLimit); + subHooks[0] = address(0); permissionsHook.installGlobalHooks(subHooks); } From 9567e9f4a18cca391d77aed72ac78bfaaf59ef79 Mon Sep 17 00:00:00 2001 From: zeroknots Date: Thu, 29 Feb 2024 08:36:53 +0700 Subject: [PATCH 7/9] feat: encoding issue --- .../src/HookMultiplex/HookMultiplexer.sol | 198 +++++++++++++----- .../src/HookMultiplex/IHookMultiplexer.sol | 10 + examples/src/HookMultiplex/ISubHook.sol | 26 ++- .../src/HookMultiplex/lib/HookEncodingLib.sol | 84 ++++++++ .../subHooks/PermissionFlags.sol | 25 ++- .../HookMultiplex/subHooks/SpendingLimits.sol | 42 ++-- .../HookMultiplex/subHooks/SubHookBase.sol | 19 ++ .../HookMultiPlexer/HookMultiplexer.t.sol | 28 ++- 8 files changed, 334 insertions(+), 98 deletions(-) create mode 100644 examples/src/HookMultiplex/IHookMultiplexer.sol create mode 100644 examples/src/HookMultiplex/lib/HookEncodingLib.sol create mode 100644 examples/src/HookMultiplex/subHooks/SubHookBase.sol 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 { From 7353e2aa8d8bff58439ffba4786247b52d728439 Mon Sep 17 00:00:00 2001 From: zeroknots Date: Fri, 1 Mar 2024 11:50:11 +0700 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=94=A5=20fix=20checks=20in=20policy?= =?UTF-8?q?=20validator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🎨 Fooq --- .../src/HookMultiplex/HookMultiplexer.sol | 6 +- examples/src/HookMultiplex/PermissionHook.sol | 63 +++ .../src/HookMultiplex/lib/HookEncodingLib.sol | 13 - .../HookMultiplex/lib/PermissionFlagLib.sol | 70 ++++ .../subHooks/PermissionFlags.sol | 161 ++++++-- .../src/PermissionsHook/PermissionsHookV2.sol | 18 +- .../HookMultiPlexer/HookMultiplexer.t.sol | 78 +++- .../PermissionsHook/PermissionsHook.t.sol.bak | 8 +- packages/PermissionManager/foundry.toml | 22 + packages/PermissionManager/package.json | 71 ++++ packages/PermissionManager/remappings.txt | 24 ++ packages/PermissionManager/src/Helper.sol | 53 +++ .../PermissionManager/src/IHookPolicy.sol | 80 ++++ packages/PermissionManager/src/IPolicy.sol | 37 ++ packages/PermissionManager/src/ISigner.sol | 31 ++ .../PermissionManager/src/PermissionHook.sol | 311 ++++++++++++++ .../src/PermissionValidator.sol | 371 +++++++++++++++++ .../PermissionManager/src/PolicyConfig.sol | 67 +++ .../src/policies/ExecutionPolicy.sol.bak | 70 ++++ .../src/policies/SudoPolicy.sol | 46 +++ .../src/signers/ECDSASigner.sol | 80 ++++ .../src/signers/WebAuthnSigner.sol.bak | 108 +++++ .../test/PermissionValidator.t.sol | 89 ++++ .../src/modules/ERC7579HookDestruct.sol | 1 + pnpm-lock.yaml | 389 +++++++++++------- 25 files changed, 2035 insertions(+), 232 deletions(-) create mode 100644 examples/src/HookMultiplex/PermissionHook.sol create mode 100644 examples/src/HookMultiplex/lib/PermissionFlagLib.sol create mode 100644 packages/PermissionManager/foundry.toml create mode 100644 packages/PermissionManager/package.json create mode 100644 packages/PermissionManager/remappings.txt create mode 100644 packages/PermissionManager/src/Helper.sol create mode 100644 packages/PermissionManager/src/IHookPolicy.sol create mode 100644 packages/PermissionManager/src/IPolicy.sol create mode 100644 packages/PermissionManager/src/ISigner.sol create mode 100644 packages/PermissionManager/src/PermissionHook.sol create mode 100644 packages/PermissionManager/src/PermissionValidator.sol create mode 100644 packages/PermissionManager/src/PolicyConfig.sol create mode 100644 packages/PermissionManager/src/policies/ExecutionPolicy.sol.bak create mode 100644 packages/PermissionManager/src/policies/SudoPolicy.sol create mode 100644 packages/PermissionManager/src/signers/ECDSASigner.sol create mode 100644 packages/PermissionManager/src/signers/WebAuthnSigner.sol.bak create mode 100644 packages/PermissionManager/test/PermissionValidator.t.sol diff --git a/examples/src/HookMultiplex/HookMultiplexer.sol b/examples/src/HookMultiplex/HookMultiplexer.sol index 9c0fce70..e74f570b 100644 --- a/examples/src/HookMultiplex/HookMultiplexer.sol +++ b/examples/src/HookMultiplex/HookMultiplexer.sol @@ -106,9 +106,9 @@ contract HookMultiPlexer is ERC7579HookDestruct, IHookMultiPlexer { for (uint256 i; i < length; i++) { bytes32 _globalHook = globalHooks[i]; - console2.logBytes32(_globalHook); - console2.log("flag", checkFlagFn(_globalHook)); - if (!checkFlagFn(_globalHook)) continue; + // console2.logBytes32(_globalHook); + // console2.log("flag", checkFlagFn(_globalHook)); + // if (!checkFlagFn(_globalHook)) continue; (bool success,) = _globalHook.decodeAddress().call(callData); if (!success) revert SubHookFailed(_globalHook); } diff --git a/examples/src/HookMultiplex/PermissionHook.sol b/examples/src/HookMultiplex/PermissionHook.sol new file mode 100644 index 00000000..b4bbb3a9 --- /dev/null +++ b/examples/src/HookMultiplex/PermissionHook.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { ERC7579HookDestruct } from "@rhinestone/modulekit/src/modules/ERC7579HookDestruct.sol"; +import { SENTINEL as SENTINELAddress, SentinelListLib } from "sentinellist/SentinelList.sol"; +import { SENTINEL as SENTINELBytes32, LinkedBytes32Lib } from "sentinellist/SentinelListBytes32.sol"; +import { TokenTransactionLib } from "./lib/TokenTransactionLib.sol"; +import { PermissionFlag, PermissionFlagLib } from "./lib/PermissionFlagLib.sol"; +import { SSTORE2 } from "solady/src/utils/SSTORE2.sol"; + +contract PermissionHook is ERC7579HookDestruct { + using SentinelListLib for SentinelListLib.SentinelList; + using LinkedBytes32Lib for LinkedBytes32Lib.LinkedBytes32; + using TokenTransactionLib for bytes4; + using PermissionFlagLib for PermissionFlag; + + error InvalidPermission(); + + struct ConfigParams { + PermissionFlag flags; + address[] allowedTargets; + bytes4[] allowedFunctions; + } + + struct ModulePermissions { + PermissionFlag flags; + LinkedBytes32Lib.LinkedBytes32 allowedFunctions; + SentinelListLib.SentinelList allowedTargets; + } + + mapping(address account => mapping(address module => ModulePermissions)) internal $permissions; + mapping(address account => mapping(address module => SentinelListLib.SentinelList subHooks)) + internal $moduleSubHooks; + + mapping(address smartAccount => LinkedBytes32Lib.LinkedBytes32 globalSubHooks) internal + $globalSubHooks; + + function configure(address module, ConfigParams memory params) public { + ModulePermissions storage $modulePermissions = $subHook().permissions[msg.sender][module]; + $modulePermissions.flags = params.flags; + + uint256 length = params.allowedTargets.length; + $modulePermissions.allowedTargets.init(); + for (uint256 i; i < length; i++) { + $modulePermissions.allowedTargets.push(params.allowedTargets[i]); + } + length = params.allowedFunctions.length; + for (uint256 i; i < length; i++) { + $modulePermissions.allowedFunctions.push(bytes32(params.allowedFunctions[i])); + } + } + + function configureWithRegistry(address module, address attester) external { + ConfigParams memory params = + abi.decode(SSTORE2.read(_getSSTORE2Ref(module, attester)), (ConfigParams)); + configure(module, params); + } + + function _getSSTORE2Ref(address module, address attester) internal pure returns (address) { + // TODO: implement actual registry lookup + return address(0xbBb6987cD1807141DBc07A9C164CAB37603Db429); + } +} diff --git a/examples/src/HookMultiplex/lib/HookEncodingLib.sol b/examples/src/HookMultiplex/lib/HookEncodingLib.sol index ad2f7262..258ddbcf 100644 --- a/examples/src/HookMultiplex/lib/HookEncodingLib.sol +++ b/examples/src/HookMultiplex/lib/HookEncodingLib.sol @@ -58,19 +58,6 @@ library HookEncodingLib { } 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; } diff --git a/examples/src/HookMultiplex/lib/PermissionFlagLib.sol b/examples/src/HookMultiplex/lib/PermissionFlagLib.sol new file mode 100644 index 00000000..b32d7af7 --- /dev/null +++ b/examples/src/HookMultiplex/lib/PermissionFlagLib.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +type PermissionFlag is bytes32; + +library PermissionFlagLib { + function pack( + bool permit_selfCall, + bool permit_moduleCall, + bool permit_sendValue, + bool permit_erc20Transfer, + bool permit_erc721Transfer, + bool permit_hasAllowedFunctions, + bool permit_hasAllowedTargets, + bool permit_moduleConfig, + bool enfoce_subhooks + ) + internal + pure + returns (PermissionFlag) + { + return PermissionFlag.wrap( + bytes32( + uint256( + (permit_selfCall ? 1 : 0) + (permit_moduleCall ? 2 : 0) + + (permit_sendValue ? 4 : 0) + (permit_erc20Transfer ? 8 : 0) + + (permit_erc721Transfer ? 16 : 0) + (permit_hasAllowedFunctions ? 32 : 0) + + (permit_hasAllowedTargets ? 64 : 0) + (permit_moduleConfig ? 128 : 0) + + (enfoce_subhooks ? 256 : 0) + ) + ) + ); + } + + function isSelfCall(PermissionFlag flags) internal pure returns (bool) { + return uint256(PermissionFlag.unwrap(flags)) & 1 == 1; + } + + function isModuleCall(PermissionFlag flags) internal pure returns (bool) { + return uint256(PermissionFlag.unwrap(flags)) & 2 == 2; + } + + function isSendValue(PermissionFlag flags) internal pure returns (bool) { + return uint256(PermissionFlag.unwrap(flags)) & 4 == 4; + } + + function isERC20Transfer(PermissionFlag flags) internal pure returns (bool) { + return uint256(PermissionFlag.unwrap(flags)) & 8 == 8; + } + + function isERC721Transfer(PermissionFlag flags) internal pure returns (bool) { + return uint256(PermissionFlag.unwrap(flags)) & 16 == 16; + } + + function hasAllowedFunctions(PermissionFlag flags) internal pure returns (bool) { + return uint256(PermissionFlag.unwrap(flags)) & 32 == 32; + } + + function hasAllowedTargets(PermissionFlag flags) internal pure returns (bool) { + return uint256(PermissionFlag.unwrap(flags)) & 64 == 64; + } + + function isModuleConfig(PermissionFlag flags) internal pure returns (bool) { + return uint256(PermissionFlag.unwrap(flags)) & 128 == 128; + } + + function enfoceSubhooks(PermissionFlag flags) internal pure returns (bool) { + return uint256(PermissionFlag.unwrap(flags)) & 256 == 256; + } +} diff --git a/examples/src/HookMultiplex/subHooks/PermissionFlags.sol b/examples/src/HookMultiplex/subHooks/PermissionFlags.sol index 30320477..fb82e3cf 100644 --- a/examples/src/HookMultiplex/subHooks/PermissionFlags.sol +++ b/examples/src/HookMultiplex/subHooks/PermissionFlags.sol @@ -1,8 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import { SentinelListLib } from "sentinellist/SentinelList.sol"; -import { LinkedBytes32Lib } from "sentinellist/SentinelListBytes32.sol"; +import { SentinelListLib, SENTINEL as SENTINEL_ADDRESS } from "sentinellist/SentinelList.sol"; +import { + LinkedBytes32Lib, SENTINEL as SENTINEL_BYTES32 +} from "sentinellist/SentinelListBytes32.sol"; +import { SSTORE2 } from "solady/src/utils/SSTORE2.sol"; import { Execution } from "@rhinestone/modulekit/src/Accounts.sol"; import { SubHookBase } from "./SubHookBase.sol"; import { TokenTransactionLib } from "../lib/TokenTransactionLib.sol"; @@ -11,31 +14,90 @@ import "forge-std/console2.sol"; // bytes32 constant STORAGE_SLOT = keccak256("permissions.storage"); bytes32 constant STORAGE_SLOT = bytes32(uint256(123)); -contract PermissionFlags is SubHookBase { +type PermissionFlags is bytes32; + +library PermissionFlagsLib { + function pack( + bool permit_selfCall, + bool permit_moduleCall, + bool permit_sendValue, + bool permit_erc20Transfer, + bool permit_erc721Transfer, + bool permit_hasAllowedFunctions, + bool permit_hasAllowedTargets, + bool permit_moduleConfig, + bool enfoce_subhooks + ) + internal + pure + returns (PermissionFlags) + { + return PermissionFlags.wrap( + bytes32( + uint256( + (permit_selfCall ? 1 : 0) + (permit_moduleCall ? 2 : 0) + + (permit_sendValue ? 4 : 0) + (permit_erc20Transfer ? 8 : 0) + + (permit_erc721Transfer ? 16 : 0) + (permit_hasAllowedFunctions ? 32 : 0) + + (permit_hasAllowedTargets ? 64 : 0) + (permit_moduleConfig ? 128 : 0) + + (enfoce_subhooks ? 256 : 0) + ) + ) + ); + } + + function isSelfCall(PermissionFlags flags) internal pure returns (bool) { + return uint256(PermissionFlags.unwrap(flags)) & 1 == 1; + } + + function isModuleCall(PermissionFlags flags) internal pure returns (bool) { + return uint256(PermissionFlags.unwrap(flags)) & 2 == 2; + } + + function isSendValue(PermissionFlags flags) internal pure returns (bool) { + return uint256(PermissionFlags.unwrap(flags)) & 4 == 4; + } + + function isERC20Transfer(PermissionFlags flags) internal pure returns (bool) { + return uint256(PermissionFlags.unwrap(flags)) & 8 == 8; + } + + function isERC721Transfer(PermissionFlags flags) internal pure returns (bool) { + return uint256(PermissionFlags.unwrap(flags)) & 16 == 16; + } + + function hasAllowedFunctions(PermissionFlags flags) internal pure returns (bool) { + return uint256(PermissionFlags.unwrap(flags)) & 32 == 32; + } + + function hasAllowedTargets(PermissionFlags flags) internal pure returns (bool) { + return uint256(PermissionFlags.unwrap(flags)) & 64 == 64; + } + + function isModuleConfig(PermissionFlags flags) internal pure returns (bool) { + return uint256(PermissionFlags.unwrap(flags)) & 128 == 128; + } + + function enfoceSubhooks(PermissionFlags flags) internal pure returns (bool) { + return uint256(PermissionFlags.unwrap(flags)) & 256 == 256; + } +} + +contract PermissionHook is SubHookBase { using SentinelListLib for SentinelListLib.SentinelList; using LinkedBytes32Lib for LinkedBytes32Lib.LinkedBytes32; using TokenTransactionLib for bytes4; + using PermissionFlagsLib for PermissionFlags; error InvalidPermission(); - struct AccessFlags { - // Execution permissions - // - Target permissions - bool selfCall; - bool moduleCall; - // - Value permissions - bool sendValue; - bool erc20Transfer; - bool erc721Transfer; - // - Calldata permissions - bool hasAllowedFunctions; - bool hasAllowedTargets; - // Module configuration permissions - bool moduleConfig; + struct ConfigParams { + PermissionFlags flags; + address[] allowedTargets; + bytes4[] allowedFunctions; } struct ModulePermissions { - AccessFlags flags; + PermissionFlags flags; LinkedBytes32Lib.LinkedBytes32 allowedFunctions; SentinelListLib.SentinelList allowedTargets; } @@ -53,28 +115,57 @@ contract PermissionFlags is SubHookBase { } } - function configure( - address module, - AccessFlags calldata flags, - address[] calldata allowedTargets, - bytes4[] calldata allowedFunctions + function getPermissions( + address account, + address module ) external + view + returns (ConfigParams memory config) { + (address[] memory allowedTargets,) = $subHook().permissions[account][module] + .allowedTargets + .getEntriesPaginated(SENTINEL_ADDRESS, 100); + (bytes32[] memory _allowedFunctions,) = $subHook().permissions[account][module] + .allowedFunctions + .getEntriesPaginated(SENTINEL_BYTES32, 100); + bytes4[] memory allowedFunctions = new bytes4[](_allowedFunctions.length); + for (uint256 i; i < _allowedFunctions.length; i++) { + allowedFunctions[i] = bytes4(_allowedFunctions[i]); + } + config = ConfigParams({ + flags: $subHook().permissions[account][module].flags, + allowedTargets: allowedTargets, + allowedFunctions: allowedFunctions + }); + } + + function configure(address module, ConfigParams memory params) public { ModulePermissions storage $modulePermissions = $subHook().permissions[msg.sender][module]; - $modulePermissions.flags = flags; + $modulePermissions.flags = params.flags; - uint256 length = allowedTargets.length; + uint256 length = params.allowedTargets.length; $modulePermissions.allowedTargets.init(); for (uint256 i; i < length; i++) { - $modulePermissions.allowedTargets.push(allowedTargets[i]); + $modulePermissions.allowedTargets.push(params.allowedTargets[i]); } - length = allowedFunctions.length; + length = params.allowedFunctions.length; for (uint256 i; i < length; i++) { - $modulePermissions.allowedFunctions.push(bytes32(allowedFunctions[i])); + $modulePermissions.allowedFunctions.push(bytes32(params.allowedFunctions[i])); } } + function _getSSTORE2Ref(address module, address attester) internal pure returns (address) { + // TODO: implement actual registry lookup + return address(0xbBb6987cD1807141DBc07A9C164CAB37603Db429); + } + + function configureWithRegistry(address module, address attester) external { + ConfigParams memory params = + abi.decode(SSTORE2.read(_getSSTORE2Ref(module, attester)), (ConfigParams)); + configure(module, params); + } + function onExecute( address smartAccount, address superVisorModule, @@ -91,12 +182,12 @@ contract PermissionFlags is SubHookBase { ModulePermissions storage $modulePermissions = $subHook().permissions[smartAccount][superVisorModule]; - AccessFlags memory flags = $modulePermissions.flags; + PermissionFlags flags = $modulePermissions.flags; bytes4 functionSig = callData.length > 4 ? bytes4(callData[0:4]) : bytes4(0); // check for self call - if (!flags.selfCall && target == smartAccount) { + if (!flags.isSelfCall() && target == smartAccount) { revert InvalidPermission(); } @@ -108,27 +199,27 @@ contract PermissionFlags is SubHookBase { // } // check for value transfer - if (!flags.sendValue && value > 0) { + if (!flags.isSendValue() && value > 0) { revert InvalidPermission(); } // Calldata permissions - if (flags.erc20Transfer && functionSig.isERC20Transfer()) { + if (flags.isERC20Transfer() && functionSig.isERC20Transfer()) { revert InvalidPermission(); } - if (flags.erc721Transfer && functionSig.isERC721Transfer()) { + if (flags.isERC721Transfer() && functionSig.isERC721Transfer()) { revert InvalidPermission(); } // check if target address is allowed to be called - if (flags.hasAllowedTargets && !$modulePermissions.allowedTargets.contains(target)) { + if (flags.hasAllowedTargets() && !$modulePermissions.allowedTargets.contains(target)) { revert InvalidPermission(); } // check if target functioni is allowed to be called if ( - flags.hasAllowedFunctions + flags.hasAllowedFunctions() && !$modulePermissions.allowedFunctions.contains(bytes32(functionSig)) ) { revert InvalidPermission(); diff --git a/examples/src/PermissionsHook/PermissionsHookV2.sol b/examples/src/PermissionsHook/PermissionsHookV2.sol index fa471062..b46a37ec 100644 --- a/examples/src/PermissionsHook/PermissionsHookV2.sol +++ b/examples/src/PermissionsHook/PermissionsHookV2.sol @@ -26,13 +26,13 @@ contract PermissionsHook is ERC7579HookDestruct, HookMultiPlexer { error InvalidPermission(); struct InitParams { - AccessFlags flags; + PermissionFlags flags; address[] allowedTargets; bytes32[] allowedFunctions; address[] moduleSubHooks; } - struct AccessFlags { + struct PermissionFlags { // Execution permissions // - Target permissions bool selfCall; @@ -49,7 +49,7 @@ contract PermissionsHook is ERC7579HookDestruct, HookMultiPlexer { } struct ModulePermissions { - AccessFlags flags; + PermissionFlags flags; LinkedBytes32Lib.LinkedBytes32 allowedFunctions; SentinelListLib.SentinelList allowedTargets; } @@ -61,8 +61,8 @@ contract PermissionsHook is ERC7579HookDestruct, HookMultiPlexer { //////////////////////////////////////////////////////////////////////////*/ function onInstall(bytes calldata data) external override { - (address[] memory _modules, AccessFlags[] memory _initParams) = - abi.decode(data, (address[], AccessFlags[])); + (address[] memory _modules, PermissionFlags[] memory _initParams) = + abi.decode(data, (address[], PermissionFlags[])); uint256 permissionsLength = _initParams.length; @@ -88,18 +88,18 @@ contract PermissionsHook is ERC7579HookDestruct, HookMultiPlexer { function addPermissions( address[] calldata _modules, - AccessFlags[] calldata _accessFlags + PermissionFlags[] calldata _PermissionFlags ) external { - uint256 permissionsLength = _accessFlags.length; + uint256 permissionsLength = _PermissionFlags.length; if (_modules.length != permissionsLength) { revert("PermissionsHook: addPermissions: module and permissions length mismatch"); } for (uint256 i; i < permissionsLength; i++) { - permissions[msg.sender][_modules[i]].flags = _accessFlags[i]; + permissions[msg.sender][_modules[i]].flags = _PermissionFlags[i]; } } @@ -257,7 +257,7 @@ contract PermissionsHook is ERC7579HookDestruct, HookMultiPlexer { ) internal { - AccessFlags memory flags = $permissions.flags; + PermissionFlags memory flags = $permissions.flags; bytes4 functionSig = callData.length > 4 ? bytes4(callData[0:4]) : bytes4(0); console2.log("validate exeute permissions"); diff --git a/examples/test/HookMultiPlexer/HookMultiplexer.t.sol b/examples/test/HookMultiPlexer/HookMultiplexer.t.sol index 7be8716e..8c47c3e4 100644 --- a/examples/test/HookMultiPlexer/HookMultiplexer.t.sol +++ b/examples/test/HookMultiPlexer/HookMultiplexer.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.23; import "forge-std/Test.sol"; import "@rhinestone/modulekit/src/ModuleKit.sol"; +import { SSTORE2 } from "solady/src/utils/SSTORE2.sol"; import { IERC20 } from "forge-std/interfaces/IERC20.sol"; import { SessionData, @@ -16,7 +17,11 @@ import { } from "@rhinestone/modulekit/src/external/ERC7579.sol"; import { IHookMultiPlexer, HookMultiPlexer, hookFlag } from "src/HookMultiPlex/HookMultiPlexer.sol"; -import { PermissionFlags } from "src/HookMultiPlex/subHooks/PermissionFlags.sol"; +import { + PermissionHook, + PermissionFlags, + PermissionFlagsLib +} from "src/HookMultiPlex/subHooks/PermissionFlags.sol"; import { SpendingLimits } from "src/HookMultiPlex/subHooks/SpendingLimits.sol"; contract HookMultiPlexerTest is RhinestoneModuleKit, Test { @@ -27,7 +32,7 @@ contract HookMultiPlexerTest is RhinestoneModuleKit, Test { AccountInstance internal instance; MockERC20 internal token; HookMultiPlexer internal multiplexer; - PermissionFlags internal permissionFlagsSubHook; + PermissionHook internal permissionFlagsSubHook; SpendingLimits internal spendingLimitsSubHook; // Mock executors @@ -43,7 +48,7 @@ contract HookMultiPlexerTest is RhinestoneModuleKit, Test { multiplexer = new HookMultiPlexer(); vm.label(address(multiplexer), "multiplexer"); - permissionFlagsSubHook = new PermissionFlags(address(multiplexer)); + permissionFlagsSubHook = new PermissionHook(address(multiplexer)); vm.label(address(permissionFlagsSubHook), "SubHook:PermissionFlags"); spendingLimitsSubHook = new SpendingLimits(address(multiplexer)); vm.label(address(spendingLimitsSubHook), "SubHook:SpendingLimits"); @@ -94,24 +99,63 @@ contract HookMultiPlexerTest is RhinestoneModuleKit, Test { } function setUpPermissionsSubHook() internal { - PermissionFlags.AccessFlags memory flags = PermissionFlags.AccessFlags({ - selfCall: false, - moduleCall: false, - hasAllowedTargets: true, - sendValue: false, - hasAllowedFunctions: true, - erc20Transfer: true, - erc721Transfer: false, - moduleConfig: false + // PermissionFlags.PermissionFlags memory flags = PermissionFlags.PermissionFlags({ + // selfCall: false, + // moduleCall: false, + // hasAllowedTargets: true, + // sendValue: false, + // hasAllowedFunctions: true, + // erc20Transfer: true, + // erc721Transfer: false, + // moduleConfig: false + // }); + + PermissionFlags flags = PermissionFlagsLib.pack({ + permit_selfCall: false, + permit_moduleCall: false, + permit_hasAllowedTargets: true, + permit_sendValue: false, + permit_hasAllowedFunctions: true, + permit_erc20Transfer: true, + permit_erc721Transfer: false, + permit_moduleConfig: false }); - vm.prank(instance.account); - permissionFlagsSubHook.configure({ - module: address(instance.defaultValidator), + flags = PermissionFlags.wrap(bytes32(uint256(type(uint256).max))); + + // vm.prank(instance.account); + // permissionFlagsSubHook.configure({ + // module: address(instance.defaultValidator), + // flags: flags, + // allowedTargets: new address[](0), + // allowedFunctions: new bytes4[](0) + // }); + + address[] memory allowedTargets = new address[](1); + allowedTargets[0] = address(0x414141414141); + // allowedTargets[1] = address(0x414141414141); + bytes4[] memory allowedFunctions = new bytes4[](1); + allowedFunctions[0] = IERC20.transfer.selector; + PermissionHook.ConfigParams memory params = PermissionHook.ConfigParams({ flags: flags, - allowedTargets: new address[](0), - allowedFunctions: new bytes4[](0) + allowedTargets: allowedTargets, + allowedFunctions: allowedFunctions }); + + bytes memory data = abi.encode(params); + + address pointer = SSTORE2.write(data); + + vm.prank(instance.account); + permissionFlagsSubHook.configureWithRegistry(address(instance.defaultValidator), pointer); + permissionFlagsSubHook.configure(address(instance.defaultValidator), params); + + PermissionHook.ConfigParams memory _params = permissionFlagsSubHook.getPermissions( + instance.account, address(instance.defaultValidator) + ); + assertEq(PermissionFlags.unwrap(params.flags), PermissionFlags.unwrap(_params.flags)); + + console2.log("pointer", pointer); } function setupSpendingLimitsSubHook() public { diff --git a/examples/test/PermissionsHook/PermissionsHook.t.sol.bak b/examples/test/PermissionsHook/PermissionsHook.t.sol.bak index ff278f93..212fc9eb 100644 --- a/examples/test/PermissionsHook/PermissionsHook.t.sol.bak +++ b/examples/test/PermissionsHook/PermissionsHook.t.sol.bak @@ -72,8 +72,8 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { modules[1] = address(executorAllowed); modules[2] = address(instance.defaultValidator); - PermissionsHook.AccessFlags[] memory permissions = new PermissionsHook.AccessFlags[](3); - permissions[0] = PermissionsHook.AccessFlags({ + PermissionsHook.PermissionFlags[] memory permissions = new PermissionsHook.PermissionFlags[](3); + permissions[0] = PermissionsHook.PermissionFlags({ selfCall: false, moduleCall: false, hasAllowedTargets: true, @@ -84,7 +84,7 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { moduleConfig: false }); - permissions[1] = PermissionsHook.AccessFlags({ + permissions[1] = PermissionsHook.PermissionFlags({ selfCall: true, moduleCall: true, hasAllowedTargets: false, @@ -95,7 +95,7 @@ contract PermissionsHookTest is RhinestoneModuleKit, Test { moduleConfig: true }); - permissions[2] = PermissionsHook.AccessFlags({ + permissions[2] = PermissionsHook.PermissionFlags({ selfCall: true, moduleCall: true, hasAllowedTargets: false, diff --git a/packages/PermissionManager/foundry.toml b/packages/PermissionManager/foundry.toml new file mode 100644 index 00000000..cc12d49c --- /dev/null +++ b/packages/PermissionManager/foundry.toml @@ -0,0 +1,22 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +fs_permissions = [{ access = "read", path = "out-optimized" }, { access = "read-write", path = "gas_calculations" }] +allow_paths = ["*", "/"] +evm_version = "cancun" +ignored_error_codes = [2394] + +[rpc_endpoints] +mainnet = "https://rpc.ankr.com/eth" + + +[fmt] +bracket_spacing = true +int_types = "long" +line_length = 100 +multiline_func_header = "all" +number_underscore = "thousands" +quote_style = "double" +tab_width = 4 +wrap_comments = true diff --git a/packages/PermissionManager/package.json b/packages/PermissionManager/package.json new file mode 100644 index 00000000..45bd5609 --- /dev/null +++ b/packages/PermissionManager/package.json @@ -0,0 +1,71 @@ +{ + "name": "permissionmanager", + "description": "ERC-7579 Port of ZeroDev's ModularPermissionValidator. Credits to ZeroDev for the original work.", + "license": "MIT", + "version": "0.1.0", + "author": { + "name": "zeroknots.eth", + "url": "https://rhinestone.wtf" + }, + "bugs": { + "url": "https://github.com/rhinestonewtf/modulekit/issues" + }, + "devDependencies": { + "@rhinestone/modulekit": "workspace:*", + "@rhinestone/sessionkeymanager": "workspace:*", + "erc4337-validation": "github:rhinestonewtf/erc4337-validation", + "@prb/math": "^4.0.2", + "@safe-global/safe-contracts": "^1.4.1", + "@openzeppelin/contracts": "5.0.1", + "@rhinestone/safe7579": "workspace:*", + "ds-test": "github:dapphub/ds-test", + "p256-verifier": "https://github.com/daimo-eth/p256-verifier", + "@ERC4337/account-abstraction": "github:kopy-kat/account-abstraction#develop", + "@ERC4337/account-abstraction-v0.6": "github:eth-infinitism/account-abstraction#v0.6.0", + "sentinellist": "github:zeroknots/sentinellist", + "erc7579": "github:erc7579/erc7579-implementation#feature/hookRevert", + "forge-std": "github:foundry-rs/forge-std", + "solady": "github:vectorized/solady", + "solhint": "^4.1.1", + "prettier": "^2.8.8" + }, + "files": [ + "artifacts", + "src", + "test/utils", + "CHANGELOG.md", + "LICENSE-GPL.md" + ], + "keywords": [ + "blockchain", + "ethereum", + "foundry", + "smart-contracts", + "solidity", + "web3" + ], + "publishConfig": { + "access": "public" + }, + "repository": "github.com/rhinestonewtf/modulekit", + "scripts": { + "fmt": "forge fmt", + "fmt:check": "forge fmt --check", + "build": "forge build && ./bytecode.sh", + "bytecode": "./bytescode.sh", + "build:optimized": "FOUNDRY_PROFILE=optimized forge build", + "build:smt": "FOUNDRY_PROFILE=smt forge build", + "clean": "rm -rf artifacts broadcast cache docs out out-optimized out-svg", + "gas:report": "forge test --gas-report --mp \"./test/integration/**/*.sol\" --nmt \"test(Fuzz)?_RevertWhen_\\w{1,}?\"", + "gas:snapshot": "forge snapshot --mp \"./test/integration/**/*.sol\" --nmt \"test(Fuzz)?_RevertWhen_\\w{1,}?\"", + "gas:snapshot:optimized": "pnpm run build:optimized && FOUNDRY_PROFILE=test-optimized forge snapshot --mp \"./test/integration/**/*.sol\" --nmt \"test(Fork)?(Fuzz)?_RevertWhen_\\w{1,}?\"", + "lint": "pnpm run lint:sol && bun run prettier:check", + "lint:sol": "forge fmt --check && pnpm solhint \"{script,src,test}/**/*.sol\"", + "prepack": "pnpm install", + "prettier:check": "prettier --check \"**/*.{json,md,svg,yml}\"", + "prettier:write": "prettier --write \"**/*.{json,md,svg,yml}\"", + "test": "forge test", + "test:lite": "FOUNDRY_PROFILE=lite forge test", + "test:optimized": "pnpm run build:optimized && FOUNDRY_PROFILE=test-optimized forge test" + } +} diff --git a/packages/PermissionManager/remappings.txt b/packages/PermissionManager/remappings.txt new file mode 100644 index 00000000..9339ed07 --- /dev/null +++ b/packages/PermissionManager/remappings.txt @@ -0,0 +1,24 @@ +solady/=node_modules/solady/ +@rhinestone/=node_modules/@rhinestone/ +erc7579/=node_modules/erc7579/src/ +@ERC4337/=node_modules/@ERC4337/ +account-abstraction/=node_modules/@ERC4337/account-abstraction/contracts/ +account-abstraction-v0.6/=node_modules/@ERC4337/account-abstraction-v0.6/contracts/ +forge-std/=node_modules/forge-std/src/ +ds-test/=node_modules/ds-test/src/ +sentinellist/=node_modules/sentinellist/src/ +ds-test/=node_modules/ds-test/src/ +forge-std/=node_modules/forge-std/src/ +account-abstraction/=node_modules/@ERC4337/account-abstraction/contracts/ +account-abstraction-v0.6/=node_modules/@ERC4337/account-abstraction-v0.6/contracts/ +@openzeppelin/=node_modules/@openzeppelin/ +erc7579/=node_modules/erc7579/src/ +sentinellist/=node_modules/sentinellist/src/ +solmate/=node_modules/solmate/src/ +solady/=node_modules/solady/ +solarray/=node_modules/solarray/src/ +@rhinestone/=node_modules/@rhinestone/ +@safe-global/=node_modules/@safe-global/ +erc4337-validation/=node_modules/erc4337-validation/src/ +@ERC4337/=node_modules/@ERC4337/ +@prb/math/=node_modules/@prb/math/ diff --git a/packages/PermissionManager/src/Helper.sol b/packages/PermissionManager/src/Helper.sol new file mode 100644 index 00000000..2b1ac4ff --- /dev/null +++ b/packages/PermissionManager/src/Helper.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ERC7579ValidatorBase } from "@rhinestone/modulekit/src/modules/ERC7579ValidatorBase.sol"; + +function _intersectValidationData( + ERC7579ValidatorBase.ValidationData a, + ERC7579ValidatorBase.ValidationData b +) + pure + returns (ERC7579ValidatorBase.ValidationData validationData) +{ + assembly { + // xor(a,b) == shows only matching bits + // and(xor(a,b), 0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff) == + // filters out the validAfter and validUntil bits + // if the result is not zero, then aggregator part is not matching + // validCase : + // a == 0 || b == 0 || xor(a,b) == 0 + // invalidCase : + // a mul b != 0 && xor(a,b) != 0 + let sum := shl(96, add(a, b)) + switch or( + iszero( + and(xor(a, b), 0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff) + ), + or(eq(sum, shl(96, a)), eq(sum, shl(96, b))) + ) + case 1 { + validationData := + and(or(a, b), 0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff) + // validAfter + let a_vd := and(0xffffffffffff0000000000000000000000000000000000000000000000000000, a) + let b_vd := and(0xffffffffffff0000000000000000000000000000000000000000000000000000, b) + validationData := or(validationData, xor(a_vd, mul(xor(a_vd, b_vd), gt(b_vd, a_vd)))) + // validUntil + a_vd := and(0x000000000000ffffffffffff0000000000000000000000000000000000000000, a) + if iszero(a_vd) { + a_vd := 0x000000000000ffffffffffff0000000000000000000000000000000000000000 + } + b_vd := and(0x000000000000ffffffffffff0000000000000000000000000000000000000000, b) + if iszero(b_vd) { + b_vd := 0x000000000000ffffffffffff0000000000000000000000000000000000000000 + } + let until := xor(a_vd, mul(xor(a_vd, b_vd), lt(b_vd, a_vd))) + if iszero(until) { + until := 0x000000000000ffffffffffff0000000000000000000000000000000000000000 + } + validationData := or(validationData, until) + } + default { validationData := 1 } + } +} diff --git a/packages/PermissionManager/src/IHookPolicy.sol b/packages/PermissionManager/src/IHookPolicy.sol new file mode 100644 index 00000000..b413cec2 --- /dev/null +++ b/packages/PermissionManager/src/IHookPolicy.sol @@ -0,0 +1,80 @@ +pragma solidity ^0.8.0; + +import { ERC7579ValidatorBase } from "@rhinestone/modulekit/src/modules/ERC7579ValidatorBase.sol"; +import { Execution } from "@rhinestone/modulekit/src/Accounts.sol"; + +interface IHookPolicy { + function registerHookPolicy( + address smartAccount, + bytes32 permissionId, + bytes calldata policyData + ) + external + payable; + + function onExecute( + address smartAccount, + address module, + address target, + uint256 value, + bytes calldata callData + ) + external + returns (bytes memory hookData); + + function onExecuteBatch( + address smartAccount, + address module, + Execution[] calldata executions + ) + external + returns (bytes memory hookData); + + function onExecuteFromExecutor( + address smartAccount, + address module, + address target, + uint256 value, + bytes calldata callData + ) + external + returns (bytes memory hookData); + + function onExecuteBatchFromExecutor( + address smartAccount, + address module, + Execution[] calldata executions + ) + external + returns (bytes memory hookData); + + /*////////////////////////////////////////////////////////////////////////// + CONFIG + //////////////////////////////////////////////////////////////////////////*/ + + function onInstallModule( + address smartAccount, + address module, + uint256 moduleType, + address moduleToInstall, + bytes calldata initData + ) + external + returns (bytes memory hookData); + + function onUninstallModule( + address smartAccount, + address module, + uint256 moduleType, + address moduleToUninstall, + bytes calldata deInitData + ) + external + returns (bytes memory hookData); + + /*////////////////////////////////////////////////////////////////////////// + POSTCHECK + //////////////////////////////////////////////////////////////////////////*/ + + function onPostCheck(bytes calldata hookData) external returns (bool success); +} diff --git a/packages/PermissionManager/src/IPolicy.sol b/packages/PermissionManager/src/IPolicy.sol new file mode 100644 index 00000000..cf80b988 --- /dev/null +++ b/packages/PermissionManager/src/IPolicy.sol @@ -0,0 +1,37 @@ +pragma solidity ^0.8.0; + +import { PackedUserOperation } from + "@ERC4337/account-abstraction/contracts/core/UserOperationLib.sol"; + +import { ERC7579ValidatorBase } from "@rhinestone/modulekit/src/modules/ERC7579ValidatorBase.sol"; + +interface IPolicy { + function registerPolicy( + address kernel, + bytes32 permissionId, + bytes calldata policyData + ) + external + payable; + function checkUserOpPolicy( + address kernel, + bytes32 permissionId, + PackedUserOperation calldata userOp, + bytes calldata proofAndSig + ) + external + payable + returns (ERC7579ValidatorBase.ValidationData); + + function validateSignature( + address kernel, + address caller, + bytes32 permissionId, + bytes32 messageHash, + bytes32 rawHash, + bytes calldata signature + ) + external + view + returns (ERC7579ValidatorBase.ValidationData); +} diff --git a/packages/PermissionManager/src/ISigner.sol b/packages/PermissionManager/src/ISigner.sol new file mode 100644 index 00000000..d9acf240 --- /dev/null +++ b/packages/PermissionManager/src/ISigner.sol @@ -0,0 +1,31 @@ +pragma solidity ^0.8.0; + +import { ERC7579ValidatorBase } from "@rhinestone/modulekit/src/modules/ERC7579ValidatorBase.sol"; + +interface ISigner { + function registerSigner( + address kernel, + bytes32 permissionId, + bytes calldata signerData + ) + external + payable; + function validateUserOp( + address kernel, + bytes32 permissionId, + bytes32 userOpHash, + bytes calldata signature + ) + external + payable + returns (ERC7579ValidatorBase.ValidationData); + function validateSignature( + address kernel, + bytes32 permissionId, + bytes32 messageHash, + bytes calldata signature + ) + external + view + returns (ERC7579ValidatorBase.ValidationData); +} diff --git a/packages/PermissionManager/src/PermissionHook.sol b/packages/PermissionManager/src/PermissionHook.sol new file mode 100644 index 00000000..3f340dd4 --- /dev/null +++ b/packages/PermissionManager/src/PermissionHook.sol @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ERC7579HookDestruct } from "@rhinestone/modulekit/src/modules/ERC7579HookDestruct.sol"; +import { IHookPolicy } from "./IHookPolicy.sol"; +import { SENTINEL, SentinelListLib } from "sentinellist/SentinelList.sol"; +import { Execution } from "@rhinestone/modulekit/src/Accounts.sol"; +import { SENTINEL as SENTINELBytes32, LinkedBytes32Lib } from "sentinellist/SentinelListBytes32.sol"; +import "forge-std/console2.sol"; + +type PolicyConfig is bytes32; + +type hookFlag is bool; + +contract PermissionHook is ERC7579HookDestruct { + using LinkedBytes32Lib for LinkedBytes32Lib.LinkedBytes32; + using HookEncodingLib for bytes32; + using HookEncodingLib for ConfigParam; + + struct ConfigParam { + hookFlag isExecutorHook; + hookFlag isValidatorHook; + hookFlag isConfigHook; + address hook; + } + + error SubHookFailed(bytes32); + + uint256 constant MAX_HOOK_NR = 16; + + mapping(address smartAccount => LinkedBytes32Lib.LinkedBytes32 globalSubHooks) internal + $globalSubHooks; + mapping(address smartAccount => mapping(address module => LinkedBytes32Lib.LinkedBytes32)) + internal $moduleSubHooks; + + function execSubHooks( + address module, + bytes memory callData, + function (bytes32) returns(bool) checkFlagFn + ) + internal + { + (bytes32[] memory globalHooks,) = + $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 = $moduleSubHooks[msg.sender][module]; + // TODO: make this nicer + if (!$moduleHooks.alreadyInitialized()) return; + + (bytes32[] memory moduleHooks,) = + $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 installGlobalHooks(ConfigParam[] calldata params) public { + uint256 length = params.length; + for (uint256 i; i < length; i++) { + ConfigParam calldata conf = params[i]; + bytes32 _packed = conf.pack(); + console2.logBytes32(_packed); + console2.log( + conf.hook, + hookFlag.unwrap(conf.isValidatorHook), + hookFlag.unwrap(conf.isExecutorHook) + ); + $globalSubHooks[msg.sender].push(_packed); + } + } + + function installModuleHooks(address module, ConfigParam[] calldata params) public { + uint256 length = params.length; + $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(_packed)) continue; + $moduleSubHooks[msg.sender][module].push(_packed); + } + } + + function onExecute( + address module, + address target, + uint256 value, + bytes calldata callData + ) + internal + virtual + override + returns (bytes memory hookData) + { + execSubHooks({ + module: module, + callData: abi.encodeCall( + IHookPolicy.onExecute, (msg.sender, module, target, value, callData) + ), + checkFlagFn: HookEncodingLib.is4337Hook + }); + } + + function onExecuteBatch( + address module, + Execution[] calldata executions + ) + internal + virtual + override + returns (bytes memory hookData) + { + execSubHooks({ + module: module, + callData: abi.encodeCall(IHookPolicy.onExecuteBatch, (msg.sender, module, executions)), + checkFlagFn: HookEncodingLib.is4337Hook + }); + } + + function onExecuteFromExecutor( + address module, + address target, + uint256 value, + bytes calldata callData + ) + internal + virtual + override + returns (bytes memory hookData) + { + execSubHooks({ + module: module, + callData: abi.encodeCall( + IHookPolicy.onExecuteFromExecutor, (msg.sender, module, target, value, callData) + ), + checkFlagFn: HookEncodingLib.isExecutorHook + }); + } + + function onExecuteBatchFromExecutor( + address module, + Execution[] calldata executions + ) + internal + virtual + override + returns (bytes memory hookData) + { + execSubHooks({ + module: module, + callData: abi.encodeCall( + IHookPolicy.onExecuteBatchFromExecutor, (msg.sender, module, executions) + ), + checkFlagFn: HookEncodingLib.isExecutorHook + }); + } + + function onInstallModule( + address module, + uint256 moduleType, + address installModule, + bytes calldata initData + ) + internal + virtual + override + returns (bytes memory hookData) + { + execSubHooks({ + module: module, + callData: abi.encodeCall( + IHookPolicy.onInstallModule, (msg.sender, module, moduleType, installModule, initData) + ), + checkFlagFn: HookEncodingLib.isConfigHook + }); + } + + function onUninstallModule( + address module, + uint256 moduleType, + address uninstallModule, + bytes calldata deInitData + ) + internal + virtual + override + returns (bytes memory hookData) + { + execSubHooks({ + module: module, + callData: abi.encodeCall( + IHookPolicy.onInstallModule, + (msg.sender, module, moduleType, uninstallModule, deInitData) + ), + checkFlagFn: HookEncodingLib.isConfigHook + }); + } + + function onPostCheck(bytes calldata hookData) + internal + virtual + override + returns (bool success) + { } + + function onInstall(bytes calldata data) external override { + $globalSubHooks[msg.sender].init(); + } + + function onUninstall(bytes calldata data) external override { + // todo + } + + function version() external pure virtual returns (string memory) { + return "1.0.0"; + } + + function name() external pure virtual returns (string memory) { + return "MultiPlexerHook"; + } + + function isModuleType(uint256 isType) external pure virtual override returns (bool) { + return isType == TYPE_HOOK; + } + + function isInitialized(address smartAccount) external view returns (bool) { + // todo + } +} + +library HookEncodingLib { + function pack(PermissionHook.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) { + 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/packages/PermissionManager/src/PermissionValidator.sol b/packages/PermissionManager/src/PermissionValidator.sol new file mode 100644 index 00000000..c74dc5d3 --- /dev/null +++ b/packages/PermissionManager/src/PermissionValidator.sol @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { + ACCOUNT_EXEC_TYPE, + ERC7579ValidatorLib +} from "@rhinestone/modulekit/src/modules/utils/ERC7579ValidatorLib.sol"; +import { ERC7579ValidatorBase } from "@rhinestone/modulekit/src/modules/ERC7579ValidatorBase.sol"; +import { + PackedUserOperation, UserOperationLib +} from "@rhinestone/modulekit/src/external/ERC4337.sol"; +import { SignatureCheckerLib } from "solady/src/utils/SignatureCheckerLib.sol"; +import { Execution } from "erc7579/interfaces/IERC7579Account.sol"; +import "./IPolicy.sol"; +import "./Helper.sol"; +import "./ISigner.sol"; +import "./PolicyConfig.sol"; + +import "forge-std/console2.sol"; + +type ValidAfter is uint48; + +type ValidUntil is uint48; + +struct Permission { + uint128 nonce; + bytes12 flag; // flag represents what permission can do + ISigner signer; + PolicyConfig firstPolicy; + ValidAfter validAfter; + ValidUntil validUntil; +} + +struct Nonce { + uint128 lastNonce; + uint128 revoked; +} + +/** + * PermissionValidator + * heavily inspired by ZeroDev's ModularPermissionValidator @author taek - zerodev + * ported to ERC-7579 by zeroknots.eth + */ +contract PermissionValidator is ERC7579ValidatorBase { + mapping(address => bytes32) public priorityPermission; + mapping(bytes32 permissionId => mapping(address smartAccount => Permission)) public permissions; + mapping( + bytes32 permissionId + => mapping(PolicyConfig policy => mapping(address smartAccount => PolicyConfig)) + ) public nextPolicy; + mapping(address smartAccount => Nonce) public nonces; + + event PermissionRegistered(address smartAccount, bytes32 permissionId); + event PermissionRevoked(address smartAccount, bytes32 permissionId); + event NonceRevoked(address smartAccount, uint256 nonce); + + function getPermissionId( + bytes12 flag, + ISigner signer, + ValidAfter validAfter, + ValidUntil validUntil, + PolicyConfig[] calldata _policyConfig, + bytes calldata signerData, + bytes[] calldata policyData + ) + public + pure + returns (bytes32) + { + return keccak256( + abi.encode( + flag, + signer, + ValidAfter.unwrap(validAfter), + ValidUntil.unwrap(validUntil), + _policyConfig, + signerData, + policyData + ) + ); + } + + function parseData(bytes calldata data) + public + pure + returns ( + uint128 nonce, + bytes12 flag, + ISigner signer, + ValidAfter validAfter, + ValidUntil validUntil, + PolicyConfig[] calldata policies, + bytes calldata signerData, + bytes[] calldata policyData + ) + { + nonce = uint128(bytes16(data[0:16])); + flag = bytes12(data[16:28]); + validAfter = ValidAfter.wrap(uint48(bytes6(data[28:34]))); + validUntil = ValidUntil.wrap(uint48(bytes6(data[34:40]))); + signer = ISigner(address(bytes20(data[40:60]))); + assembly { + let offset := add(data.offset, 60) + policies.offset := add(add(offset, 32), calldataload(offset)) + policies.length := calldataload(sub(policies.offset, 32)) + signerData.offset := add(add(offset, 32), calldataload(add(offset, 32))) + signerData.length := calldataload(sub(signerData.offset, 32)) + policyData.offset := add(add(offset, 32), calldataload(add(offset, 64))) + policyData.length := calldataload(sub(policyData.offset, 32)) + } + } + + function enable(bytes calldata data) external payable { + ( + uint128 nonce, + bytes12 flag, + ISigner signer, + ValidAfter validAfter, + ValidUntil validUntil, + PolicyConfig[] calldata policies, + bytes calldata signerData, + bytes[] calldata policyData + ) = parseData(data); + registerPermission( + nonce, flag, signer, validAfter, validUntil, policies, signerData, policyData + ); + } + + function registerPermission( + uint128 nonce, + bytes12 flag, + ISigner signer, + ValidAfter validAfter, + ValidUntil validUntil, + PolicyConfig[] calldata policy, + bytes calldata signerData, + bytes[] calldata policyData + ) + public + payable + returns (bytes32 permissionId) + { + require(flag != toFlag(0), "flag should not be empty"); + require( + nonce == nonces[msg.sender].lastNonce || nonce == nonces[msg.sender].lastNonce + 1, + "nonce should be next" + ); + nonces[msg.sender].lastNonce++; + permissionId = + getPermissionId(flag, signer, validAfter, validUntil, policy, signerData, policyData); + if (flag == MAX_FLAG) { + priorityPermission[msg.sender] = permissionId; + } + + bytes12 maxFlag = flag; + for (uint256 i = 0; i < policy.length; i++) { + //TODO make sure address of the policy is sorted + PolicyConfigLib.getAddress(policy[i]).registerPolicy( + msg.sender, permissionId, policyData[i] + ); + // NOTE: flag for policy is inverted version of flag for permission; + bytes12 currentFlag = PolicyConfigLib.getFlags(policy[i]); + // turn off flags that are used, + // meaning that remaining maxFlag will indicate the permissions that are not used on + // this permission + maxFlag = currentFlag & maxFlag; + } + signer.registerSigner(msg.sender, permissionId, signerData); + + PolicyConfig firstPolicy = policy[0]; // NOTE : policy should not be empty array + require(maxFlag == bytes12(0), "error : permission flag exceeds policy flag"); + permissions[permissionId][msg.sender] = + Permission(nonce, flag, signer, firstPolicy, validAfter, validUntil); + for (uint256 i = 1; i < policy.length; i++) { + nextPolicy[permissionId][policy[i - 1]][msg.sender] = policy[i]; + } + emit PermissionRegistered(msg.sender, permissionId); + } + + function disable(bytes calldata data) external payable { + if (data.length == 32) { + revokePermission(bytes32(data)); + } else { + revokePermission(uint128(bytes16(data))); + } + } + + function revokePermission(bytes32 permissionId) public payable { + permissions[permissionId][msg.sender].flag = toFlag(0); // NOTE: making flag == 0 makes it + // invalid + emit PermissionRevoked(msg.sender, permissionId); + } + + function revokePermission(uint128 nonce) public payable { + nonces[msg.sender].revoked = nonce; + emit NonceRevoked(msg.sender, nonce); + } + + function validateUserOp( + PackedUserOperation calldata _userOp, + bytes32 _userOpHash + ) + external + override + returns (ValidationData validationData) + { + require(_userOp.sender == msg.sender, "sender must be msg.sender"); + bytes32 permissionId = bytes32(_userOp.signature[0:32]); + console2.log("validate user Op. PermissionID:"); + console2.logBytes32(permissionId); + if ( + permissions[permissionId][msg.sender].flag & toFlag(1) == toFlag(0) + || nonces[msg.sender].revoked > permissions[permissionId][msg.sender].nonce + ) { + return VALIDATION_FAILED; + } + Permission memory permission = permissions[permissionId][msg.sender]; + PolicyConfig policy = permission.firstPolicy; + uint256 cursor = 32; + console2.log("policy", address(PolicyConfigLib.getAddress(policy))); + console2.logBytes32(PolicyConfig.unwrap(policy)); + while (address(PolicyConfigLib.getAddress(policy)) != address(0)) { + // if (PolicyConfigLib.skipOnValidateUserOp(policy)) { + // policy = nextPolicy[permissionId][policy][msg.sender]; + // console2.log("skip"); + // continue; + // } + bytes calldata policyData; + console2.log("cursor", cursor); + + if ( + _userOp.signature.length >= cursor + 52 + && address(bytes20(_userOp.signature[cursor:cursor + 20])) + == address(PolicyConfigLib.getAddress(policy)) + ) { + console2.log("if"); + // only when policy address is same as the one in signature + uint256 length = uint256(bytes32(_userOp.signature[cursor + 20:cursor + 52])); + require( + _userOp.signature.length >= cursor + 52 + length, + "policyData length exceeds signature length" + ); + policyData = _userOp.signature[cursor + 52:cursor + 52 + length]; // [policyAddress, + // policyDataLength, policyData] + cursor += 52 + length; + } else { + console2.log("else"); + policyData = _userOp.signature[cursor:cursor]; + } + + console2.log("policyData"); + console2.logBytes(policyData); + ValidationData policyValidation = PolicyConfigLib.getAddress(policy).checkUserOpPolicy( + msg.sender, permissionId, _userOp, policyData + ); + validationData = _intersectValidationData(validationData, policyValidation); + policy = nextPolicy[permissionId][policy][msg.sender]; + } + ValidationData signatureValidation = permission.signer.validateUserOp( + msg.sender, permissionId, _userOpHash, _userOp.signature[cursor:] + ); + validationData = _intersectValidationData(validationData, signatureValidation); + } + + struct ValidationSigMemory { + address caller; + bytes32 permissionId; + bytes32 rawHash; + uint256 cursor; + PolicyConfig policy; + } + + function validateSignature( + bytes32 hash, + bytes calldata signature + ) + external + view + returns (ERC7579ValidatorBase.ValidationData validationData) + { + ValidationSigMemory memory sigMemory; + sigMemory.permissionId = bytes32(signature[0:32]); + if ( + nonces[msg.sender].revoked > permissions[sigMemory.permissionId][msg.sender].nonce + || permissions[sigMemory.permissionId][msg.sender].flag & toFlag(2) == toFlag(0) + ) { + return VALIDATION_FAILED; + } + Permission memory permission = permissions[sigMemory.permissionId][msg.sender]; + // signature should be packed with + // (permissionId, [proof || signature]) + // (permissionId, [ (policyAddress) + (policyProof) || signature] + bytes calldata proofAndSignature; //) = abi.decode(signature[32:], (bytes, bytes)); + assembly { + proofAndSignature.offset := add(signature.offset, 32) + proofAndSignature.length := sub(signature.length, 32) + } + + sigMemory.cursor = 0; + sigMemory.policy = permission.firstPolicy; + sigMemory.caller = address(bytes20(msg.data[msg.data.length - 20:])); + sigMemory.rawHash = bytes32(msg.data[msg.data.length - 52:msg.data.length - 20]); + while (address(PolicyConfigLib.getAddress(sigMemory.policy)) != address(0)) { + if (PolicyConfigLib.skipOnValidateSignature(sigMemory.policy)) { + sigMemory.policy = nextPolicy[sigMemory.permissionId][sigMemory.policy][msg.sender]; + continue; + } + bytes calldata policyData; + if ( + address(bytes20(proofAndSignature[sigMemory.cursor:sigMemory.cursor + 20])) + == address(PolicyConfigLib.getAddress(sigMemory.policy)) + ) { + // only when policy address is same as the one in signature + uint256 length = + uint256(bytes32(proofAndSignature[sigMemory.cursor + 20:sigMemory.cursor + 52])); + policyData = proofAndSignature[sigMemory.cursor + 52:]; // [policyAddress, + // policyDataLength, policyData] + sigMemory.cursor += 52 + length; + } else { + policyData = proofAndSignature[sigMemory.cursor:sigMemory.cursor]; + // not move cursor here + } + ValidationData policyValidation = PolicyConfigLib.getAddress(sigMemory.policy) + .validateSignature( + msg.sender, + sigMemory.caller, + sigMemory.permissionId, + hash, + sigMemory.rawHash, + policyData + ); + validationData = _intersectValidationData(validationData, policyValidation); + sigMemory.policy = nextPolicy[sigMemory.permissionId][sigMemory.policy][msg.sender]; + } + ValidationData signatureValidation = permission.signer.validateSignature( + msg.sender, sigMemory.permissionId, hash, proofAndSignature[sigMemory.cursor:] + ); + validationData = _intersectValidationData(validationData, signatureValidation); + } + + function version() external pure virtual returns (string memory) { + return "1.0.0"; + } + + function name() external pure virtual returns (string memory) { + return "PermissionManager"; + } + + function isModuleType(uint256 isType) external pure virtual override returns (bool) { + return isType == TYPE_VALIDATOR; + } + + function onInstall(bytes calldata data) external override { } + function onUninstall(bytes calldata data) external override { } + + function isValidSignatureWithSender( + address sender, + bytes32 hash, + bytes calldata data + ) + external + view + virtual + override + returns (bytes4) + { } + + function isInitialized(address smartAccount) external view returns (bool) { + return false; + } +} diff --git a/packages/PermissionManager/src/PolicyConfig.sol b/packages/PermissionManager/src/PolicyConfig.sol new file mode 100644 index 00000000..9ac1da26 --- /dev/null +++ b/packages/PermissionManager/src/PolicyConfig.sol @@ -0,0 +1,67 @@ +pragma solidity ^0.8.0; + +import "./IPolicy.sol"; + +type PolicyConfig is bytes32; + +function toFlag(uint256 x) pure returns (bytes12) { + return bytes12(bytes32(x << 160)); +} + +function toPermissionFlag(uint256 x) pure returns (bytes12) { + bytes12 ret = bytes12(bytes32(x << 160)); + assembly { + ret := not(ret) + } + return ret; +} + +bytes12 constant MAX_FLAG = 0xffffffffffffffffffffffff; +// PolicyData is a 32 bytes array that contains the address of the policy +// [flags(12 bytes), address(20 bytes)] +// flags is 96 bits that contains the following information +// from last to first bit +// 1 bit : not for validatUserOp +// 1 bit : not for validateSignature +// 1 bit : not for validateCaller + +library PolicyConfigLib { + function pack(IPolicy addr, bytes12 flag) internal pure returns (PolicyConfig data) { + assembly { + data := or(addr, flag) + } + } + + function getAddress(PolicyConfig data) internal pure returns (IPolicy policy) { + assembly { + policy := and(data, 0xffffffffffffffffffffffffffffffffffffffff) + } + } + + function getFlags(PolicyConfig data) internal pure returns (bytes12 flags) { + assembly { + flags := shr(160, data) + } + } + + function skipOnValidateUserOp(PolicyConfig data) internal pure returns (bool result) { + assembly { + let flags := shr(160, data) + result := and(flags, 0x1) + } + } + + function skipOnValidateSignature(PolicyConfig data) internal pure returns (bool result) { + assembly { + let flags := shr(161, data) + result := and(flags, 0x1) + } + } + + function skipOnValidateCaller(PolicyConfig data) internal pure returns (bool result) { + assembly { + let flags := shr(162, data) + result := and(flags, 0x1) + } + } +} diff --git a/packages/PermissionManager/src/policies/ExecutionPolicy.sol.bak b/packages/PermissionManager/src/policies/ExecutionPolicy.sol.bak new file mode 100644 index 00000000..cc54c509 --- /dev/null +++ b/packages/PermissionManager/src/policies/ExecutionPolicy.sol.bak @@ -0,0 +1,70 @@ +pragma solidity ^0.8.0; + +import "../IPolicy.sol"; +import { ValidAfter } from "../PermissionManager.sol"; + +struct ExecutionConfig { + uint48 interval; + uint48 count; + ValidAfter startAt; +} + +contract ExecutePolicy is IPolicy { + mapping( + address permissionValidator + => mapping(bytes32 permissionId => mapping(address kernel => ExecutionConfig)) + ) public executionConfigs; + + function registerPolicy( + address kernel, + bytes32 permissionId, + bytes calldata policyData + ) + external + payable + override + { + uint48 delay = uint48(bytes6(policyData[0:6])); + uint48 count = uint48(bytes6(policyData[6:12])); + uint48 startAt = uint48(bytes6(policyData[12:18])); + executionConfigs[msg.sender][permissionId][kernel] = + ExecutionConfig(delay, count, ValidAfter.wrap(startAt)); + } + + function checkUserOpPolicy( + address kernel, + bytes32 permissionId, + UserOperation calldata, + bytes calldata + ) + external + payable + override + returns (ValidationData) + { + ExecutionConfig memory config = executionConfigs[msg.sender][permissionId][kernel]; + if (config.count == 0) { + return SIG_VALIDATION_FAILED; + } + executionConfigs[msg.sender][permissionId][kernel].count = config.count - 1; + executionConfigs[msg.sender][permissionId][kernel].startAt = + ValidAfter.wrap(ValidAfter.unwrap(config.startAt) + config.interval); + return packValidationData(config.startAt, ValidUntil.wrap(0)); + } + + function validateSignature( + address, + address, + bytes32, + bytes32, + bytes32, + bytes calldata + ) + external + view + override + returns (ValidationData) + { + return ValidationData.wrap(0); + } +} diff --git a/packages/PermissionManager/src/policies/SudoPolicy.sol b/packages/PermissionManager/src/policies/SudoPolicy.sol new file mode 100644 index 00000000..67e00805 --- /dev/null +++ b/packages/PermissionManager/src/policies/SudoPolicy.sol @@ -0,0 +1,46 @@ +pragma solidity ^0.8.0; + +import { IPolicy, PackedUserOperation } from "../IPolicy.sol"; +import { ERC7579ValidatorBase } from "@rhinestone/modulekit/src/modules/ERC7579ValidatorBase.sol"; + +contract SudoPolicy is IPolicy { + function registerPolicy( + address kernel, + bytes32 permissionId, + bytes calldata data + ) + external + payable + override + { } + + function checkUserOpPolicy( + address kernel, + bytes32 permissionId, + PackedUserOperation calldata userOp, + bytes calldata + ) + external + payable + override + returns (ERC7579ValidatorBase.ValidationData) + { + return ERC7579ValidatorBase.ValidationData.wrap(0); + } + + function validateSignature( + address kernel, + address caller, + bytes32 permissionId, + bytes32 messageHash, + bytes32 rawHash, + bytes calldata signature + ) + external + view + override + returns (ERC7579ValidatorBase.ValidationData) + { + return ERC7579ValidatorBase.ValidationData.wrap(0); + } +} diff --git a/packages/PermissionManager/src/signers/ECDSASigner.sol b/packages/PermissionManager/src/signers/ECDSASigner.sol new file mode 100644 index 00000000..83ac37d1 --- /dev/null +++ b/packages/PermissionManager/src/signers/ECDSASigner.sol @@ -0,0 +1,80 @@ +pragma solidity ^0.8.0; + +import { ECDSA } from "solady/src/utils/ECDSA.sol"; +import { ERC7579ValidatorBase } from "@rhinestone/modulekit/src/modules/ERC7579ValidatorBase.sol"; +import { ISigner } from "../ISigner.sol"; + +import "forge-std/console2.sol"; + +contract ECDSASigner is ISigner { + using ECDSA for bytes32; + + mapping( + address caller => mapping(bytes32 permissionId => mapping(address smartAccount => address)) + ) public signer; + + function registerSigner( + address smartAccount, + bytes32 permissionId, + bytes calldata data + ) + external + payable + override + { + console2.logBytes(data); + require( + signer[msg.sender][permissionId][smartAccount] == address(0), + "ECDSASigner: smartAccount already registered" + ); + require(data.length == 20, "ECDSASigner: invalid signer address"); + address signerAddress = address(bytes20(data[0:20])); + signer[msg.sender][permissionId][smartAccount] = signerAddress; + } + + function validateUserOp( + address smartAccount, + bytes32 permissionId, + bytes32 userOpHash, + bytes calldata signature + ) + external + payable + override + returns (ERC7579ValidatorBase.ValidationData) + { + require( + signer[msg.sender][permissionId][smartAccount] != address(0), + "ECDSASigner: smartAccount not registered" + ); + address recovered = ECDSA.toEthSignedMessageHash(userOpHash).recover(signature); + if (recovered == signer[msg.sender][permissionId][smartAccount]) { + return ERC7579ValidatorBase.ValidationData.wrap(0); + } + return ERC7579ValidatorBase.ValidationData.wrap(1); + } + + function validateSignature( + address smartAccount, + bytes32 permissionId, + bytes32 messageHash, + bytes calldata signature + ) + external + view + override + returns (ERC7579ValidatorBase.ValidationData) + { + address signerAddress = signer[msg.sender][permissionId][smartAccount]; + require(signerAddress != address(0), "ECDSASigner: smartAccount not registered"); + if (messageHash.recover(signature) == signerAddress) { + return ERC7579ValidatorBase.ValidationData.wrap(0); + } + bytes32 ethHash = ECDSA.toEthSignedMessageHash(messageHash); + address recovered = ethHash.recover(signature); + if (recovered == signerAddress) { + return ERC7579ValidatorBase.ValidationData.wrap(0); + } + return ERC7579ValidatorBase.ValidationData.wrap(1); + } +} diff --git a/packages/PermissionManager/src/signers/WebAuthnSigner.sol.bak b/packages/PermissionManager/src/signers/WebAuthnSigner.sol.bak new file mode 100644 index 00000000..40c8cd57 --- /dev/null +++ b/packages/PermissionManager/src/signers/WebAuthnSigner.sol.bak @@ -0,0 +1,108 @@ +pragma solidity ^0.8.0; + +import { WebAuthn } from "p256-verifier/WebAuthn.sol"; +import { ISigner } from "../ISigner.sol"; +import { ValidationData } from "../../../common/Types.sol"; +import { SIG_VALIDATION_FAILED } from "../../../common/Constants.sol"; + +struct WebAuthnValidatorData { + uint256 x; + uint256 y; +} + +contract WebAuthnSigner is ISigner { + mapping( + address caller + => mapping(bytes32 permissionId => mapping(address kernel => WebAuthnValidatorData)) + ) public webAuthnValidatorStorage; + + function registerSigner( + address kernel, + bytes32 permissionId, + bytes calldata data + ) + external + payable + override + { + WebAuthnValidatorData memory pubKey = abi.decode(data, (WebAuthnValidatorData)); + require( + webAuthnValidatorStorage[msg.sender][permissionId][kernel].x == 0 + && webAuthnValidatorStorage[msg.sender][permissionId][kernel].y == 0, + "WebAuthnSigner: kernel already registered" + ); + require(pubKey.x != 0 && pubKey.y != 0, "WebAuthnSigner: invalid public key"); + webAuthnValidatorStorage[msg.sender][permissionId][kernel] = pubKey; + } + + function validateUserOp( + address kernel, + bytes32 permissionId, + bytes32 userOpHash, + bytes calldata signature + ) + external + payable + override + returns (ValidationData) + { + return _verifySignature(kernel, permissionId, userOpHash, signature); + } + + function validateSignature( + address kernel, + bytes32 permissionId, + bytes32 messageHash, + bytes calldata signature + ) + external + view + override + returns (ValidationData) + { + return _verifySignature(kernel, permissionId, messageHash, signature); + } + + function _verifySignature( + address sender, + bytes32 permissionId, + bytes32 hash, + bytes calldata signature + ) + private + view + returns (ValidationData) + { + ( + bytes memory authenticatorData, + string memory clientDataJSON, + uint256 challengeLocation, + uint256 responseTypeLocation, + uint256 r, + uint256 s + ) = abi.decode(signature, (bytes, string, uint256, uint256, uint256, uint256)); + + WebAuthnValidatorData memory pubKey = + webAuthnValidatorStorage[msg.sender][permissionId][sender]; + require(pubKey.x != 0 && pubKey.y != 0, "WebAuthnSigner: kernel not registered"); + + bool isValid = WebAuthn.verifySignature( + abi.encodePacked(hash), + authenticatorData, + true, // TODO: check if this needs to be true always + clientDataJSON, + challengeLocation, + responseTypeLocation, + r, + s, + pubKey.x, + pubKey.y + ); + + if (isValid) { + return ValidationData.wrap(0); + } + + return SIG_VALIDATION_FAILED; + } +} diff --git a/packages/PermissionManager/test/PermissionValidator.t.sol b/packages/PermissionManager/test/PermissionValidator.t.sol new file mode 100644 index 00000000..31239dea --- /dev/null +++ b/packages/PermissionManager/test/PermissionValidator.t.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "forge-std/Test.sol"; +import "@rhinestone/modulekit/src/ModuleKit.sol"; +import "@rhinestone/modulekit/src/Helpers.sol"; +import "@rhinestone/modulekit/src/Core.sol"; + +import { MODULE_TYPE_VALIDATOR } from "@rhinestone/modulekit/src/external/ERC7579.sol"; + +import "src/PermissionValidator.sol"; +import "src/policies/SudoPolicy.sol"; +import "src/signers/ECDSASigner.sol"; + +contract PermissionValidatorTest is RhinestoneModuleKit, Test { + using ModuleKitHelpers for *; + using ModuleKitSCM for *; + using ModuleKitUserOp for *; + + AccountInstance internal instance; + Account internal signer = makeAccount("signer"); + PermissionValidator internal permissionValidator; + + SudoPolicy internal sudoPolicy; + ECDSASigner internal ecdsaSigner; + + bytes32 permissionId; + + function setUp() public { + vm.warp(123_123_123); + instance = makeAccountInstance("instance"); + vm.deal(instance.account, 100 ether); + + permissionValidator = new PermissionValidator(); + vm.label(address(permissionValidator), "PermissionValidator"); + sudoPolicy = new SudoPolicy(); + vm.label(address(sudoPolicy), "SudoPolicy"); + ecdsaSigner = new ECDSASigner(); + vm.label(address(ecdsaSigner), "ECDSASigner"); + + _setupPermission(); + instance.installModule({ + module: address(permissionValidator), + moduleTypeId: MODULE_TYPE_VALIDATOR, + data: "" + }); + } + + function _setupPermission() internal { + vm.prank(instance.account); + + bytes12 flag = MAX_FLAG; + bytes memory signerData = abi.encodePacked(signer.addr); + PolicyConfig[] memory policyConfigs = new PolicyConfig[](1); + policyConfigs[0] = PolicyConfigLib.pack({ addr: IPolicy(address(sudoPolicy)), flag: flag }); + bytes[] memory policyDatas = new bytes[](1); + policyDatas[0] = hex"41414141"; + permissionId = permissionValidator.registerPermission({ + nonce: 0, + flag: flag, + signer: ISigner(address(ecdsaSigner)), + validAfter: ValidAfter.wrap(uint48(block.timestamp - 1)), + validUntil: ValidUntil.wrap(type(uint48).max), + policy: policyConfigs, + signerData: signerData, + policyData: policyDatas + }); + } + + function test_sendETH() public { + address target = makeAddr("target"); + uint256 balanceBefore = target.balance; + uint256 value = 1 ether; + bytes memory callData = ""; + + // instance.exec({ target: address(target), value: value, callData: callData }); + + UserOpData memory userOpData = + instance.getExecOps(target, value, callData, address(permissionValidator)); + // sign userOp with default signature + userOpData.userOp.signature = abi.encodePacked( + permissionId, ecdsaSign(signer.key, ECDSA.toEthSignedMessageHash(userOpData.userOpHash)) + ); + + // send userOp to entrypoint + userOpData.execUserOps(); + assertEq(target.balance, balanceBefore + value); + } +} diff --git a/packages/modulekit/src/modules/ERC7579HookDestruct.sol b/packages/modulekit/src/modules/ERC7579HookDestruct.sol index 923ddae1..b42c9dd2 100644 --- a/packages/modulekit/src/modules/ERC7579HookDestruct.sol +++ b/packages/modulekit/src/modules/ERC7579HookDestruct.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.23; import { ERC7579HookBase } from "./ERC7579HookBase.sol"; import { IERC7579Account } from "../Accounts.sol"; import { ExecutionLib, Execution } from "erc7579/lib/ExecutionLib.sol"; +import { PackedUserOperation } from "../external/ERC4337.sol"; import { ModeLib, CallType, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7de6ea5..d4afe7e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -135,6 +135,60 @@ importers: specifier: github:transmissions11/solmate version: github.com/transmissions11/solmate/c892309933b25c03d32b1b0d674df7ae292ba925 + packages/PermissionManager: + devDependencies: + '@ERC4337/account-abstraction': + specifier: github:kopy-kat/account-abstraction#develop + version: github.com/kopy-kat/account-abstraction/396bda190ddc00919339986c72c580c160a15587(ethers@5.4.0)(hardhat@2.20.1)(lodash@4.17.21)(typechain@5.2.0) + '@ERC4337/account-abstraction-v0.6': + specifier: github:eth-infinitism/account-abstraction#v0.6.0 + version: github.com/eth-infinitism/account-abstraction/7174d6d845618dbd11cee68eefa715f5263690b6(ethers@5.4.0)(hardhat@2.20.1)(lodash@4.17.21)(typechain@5.2.0) + '@openzeppelin/contracts': + specifier: 5.0.1 + version: 5.0.1 + '@prb/math': + specifier: ^4.0.2 + version: 4.0.2 + '@rhinestone/modulekit': + specifier: workspace:* + version: link:../modulekit + '@rhinestone/safe7579': + specifier: workspace:* + version: link:../../accounts/safe7579 + '@rhinestone/sessionkeymanager': + specifier: workspace:* + version: link:../SessionKeyManager + '@safe-global/safe-contracts': + specifier: ^1.4.1 + version: 1.4.1(ethers@5.4.0) + ds-test: + specifier: github:dapphub/ds-test + version: github.com/dapphub/ds-test/e282159d5170298eb2455a6c05280ab5a73a4ef0 + erc4337-validation: + specifier: github:rhinestonewtf/erc4337-validation + version: github.com/rhinestonewtf/erc4337-validation/5bc7fa2dec4ed815d389c41df0c93151e1ff6cae + erc7579: + specifier: github:erc7579/erc7579-implementation#feature/hookRevert + version: github.com/erc7579/erc7579-implementation/240e114638eaa10facc01cd8cb6d7a32e1c63269 + forge-std: + specifier: github:foundry-rs/forge-std + version: github.com/foundry-rs/forge-std/2f6762e4f73f3d835457c220b5f62dfeeb6f6341 + p256-verifier: + specifier: https://github.com/daimo-eth/p256-verifier + version: github.com/daimo-eth/p256-verifier/29475ae300ec95d98d5c7cc34c094846f0aa2dcd + prettier: + specifier: ^2.8.8 + version: 2.8.8 + sentinellist: + specifier: github:zeroknots/sentinellist + version: github.com/zeroknots/sentinellist/5f851f29b5d5e0fd4f5cdc63a2ccd7865ab1802d + solady: + specifier: github:vectorized/solady + version: github.com/vectorized/solady/d699161248fdb571a35fe12f4bd3077032f33806 + solhint: + specifier: ^4.1.1 + version: 4.1.1(typescript@5.3.3) + packages/SessionKeyManager: devDependencies: '@ERC4337/account-abstraction': @@ -261,15 +315,15 @@ packages: /@ethersproject/abi@5.4.0: resolution: {integrity: sha512-9gU2H+/yK1j2eVMdzm6xvHSnMxk8waIHQGYCZg5uvAyH0rsAzxkModzBSpbAkAuhKFEovC2S9hM4nPuLym8IZw==} dependencies: - '@ethersproject/address': 5.4.0 - '@ethersproject/bignumber': 5.4.0 - '@ethersproject/bytes': 5.4.0 - '@ethersproject/constants': 5.4.0 - '@ethersproject/hash': 5.4.0 - '@ethersproject/keccak256': 5.4.0 - '@ethersproject/logger': 5.4.0 - '@ethersproject/properties': 5.4.0 - '@ethersproject/strings': 5.4.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/hash': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/strings': 5.7.0 dev: true /@ethersproject/abi@5.7.0: @@ -289,13 +343,13 @@ packages: /@ethersproject/abstract-provider@5.4.0: resolution: {integrity: sha512-vPBR7HKUBY0lpdllIn7tLIzNN7DrVnhCLKSzY0l8WAwxz686m/aL7ASDzrVxV93GJtIub6N2t4dfZ29CkPOxgA==} dependencies: - '@ethersproject/bignumber': 5.4.0 - '@ethersproject/bytes': 5.4.0 - '@ethersproject/logger': 5.4.0 - '@ethersproject/networks': 5.4.0 - '@ethersproject/properties': 5.4.0 - '@ethersproject/transactions': 5.4.0 - '@ethersproject/web': 5.4.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/networks': 5.7.1 + '@ethersproject/properties': 5.7.0 + '@ethersproject/transactions': 5.7.0 + '@ethersproject/web': 5.7.1 dev: true /@ethersproject/abstract-provider@5.7.0: @@ -313,11 +367,11 @@ packages: /@ethersproject/abstract-signer@5.4.0: resolution: {integrity: sha512-AieQAzt05HJZS2bMofpuxMEp81AHufA5D6M4ScKwtolj041nrfIbIi8ciNW7+F59VYxXq+V4c3d568Q6l2m8ew==} dependencies: - '@ethersproject/abstract-provider': 5.4.0 - '@ethersproject/bignumber': 5.4.0 - '@ethersproject/bytes': 5.4.0 - '@ethersproject/logger': 5.4.0 - '@ethersproject/properties': 5.4.0 + '@ethersproject/abstract-provider': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 dev: true /@ethersproject/abstract-signer@5.7.0: @@ -333,11 +387,11 @@ packages: /@ethersproject/address@5.4.0: resolution: {integrity: sha512-SD0VgOEkcACEG/C6xavlU1Hy3m5DGSXW3CUHkaaEHbAPPsgi0coP5oNPsxau8eTlZOk/bpa/hKeCNoK5IzVI2Q==} dependencies: - '@ethersproject/bignumber': 5.4.0 - '@ethersproject/bytes': 5.4.0 - '@ethersproject/keccak256': 5.4.0 - '@ethersproject/logger': 5.4.0 - '@ethersproject/rlp': 5.4.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/rlp': 5.7.0 dev: true /@ethersproject/address@5.7.0: @@ -353,7 +407,7 @@ packages: /@ethersproject/base64@5.4.0: resolution: {integrity: sha512-CjQw6E17QDSSC5jiM9YpF7N1aSCHmYGMt9bWD8PWv6YPMxjsys2/Q8xLrROKI3IWJ7sFfZ8B3flKDTM5wlWuZQ==} dependencies: - '@ethersproject/bytes': 5.4.0 + '@ethersproject/bytes': 5.7.0 dev: true /@ethersproject/base64@5.7.0: @@ -365,8 +419,8 @@ packages: /@ethersproject/basex@5.4.0: resolution: {integrity: sha512-J07+QCVJ7np2bcpxydFVf/CuYo9mZ7T73Pe7KQY4c1lRlrixMeblauMxHXD0MPwFmUHZIILDNViVkykFBZylbg==} dependencies: - '@ethersproject/bytes': 5.4.0 - '@ethersproject/properties': 5.4.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/properties': 5.7.0 dev: true /@ethersproject/basex@5.7.0: @@ -379,8 +433,8 @@ packages: /@ethersproject/bignumber@5.4.0: resolution: {integrity: sha512-OXUu9f9hO3vGRIPxU40cignXZVaYyfx6j9NNMjebKdnaCL3anCLSSy8/b8d03vY6dh7duCC0kW72GEC4tZer2w==} dependencies: - '@ethersproject/bytes': 5.4.0 - '@ethersproject/logger': 5.4.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 bn.js: 4.12.0 dev: true @@ -395,7 +449,7 @@ packages: /@ethersproject/bytes@5.4.0: resolution: {integrity: sha512-H60ceqgTHbhzOj4uRc/83SCN9d+BSUnOkrr2intevqdtEMO1JFVZ1XL84OEZV+QjV36OaZYxtnt4lGmxcGsPfA==} dependencies: - '@ethersproject/logger': 5.4.0 + '@ethersproject/logger': 5.7.0 dev: true /@ethersproject/bytes@5.7.0: @@ -407,7 +461,7 @@ packages: /@ethersproject/constants@5.4.0: resolution: {integrity: sha512-tzjn6S7sj9+DIIeKTJLjK9WGN2Tj0P++Z8ONEIlZjyoTkBuODN+0VfhAyYksKi43l1Sx9tX2VlFfzjfmr5Wl3Q==} dependencies: - '@ethersproject/bignumber': 5.4.0 + '@ethersproject/bignumber': 5.7.0 dev: true /@ethersproject/constants@5.7.0: @@ -419,16 +473,16 @@ packages: /@ethersproject/contracts@5.4.0: resolution: {integrity: sha512-hkO3L3IhS1Z3ZtHtaAG/T87nQ7KiPV+/qnvutag35I0IkiQ8G3ZpCQ9NNOpSCzn4pWSW4CfzmtE02FcqnLI+hw==} dependencies: - '@ethersproject/abi': 5.4.0 - '@ethersproject/abstract-provider': 5.4.0 - '@ethersproject/abstract-signer': 5.4.0 - '@ethersproject/address': 5.4.0 - '@ethersproject/bignumber': 5.4.0 - '@ethersproject/bytes': 5.4.0 - '@ethersproject/constants': 5.4.0 - '@ethersproject/logger': 5.4.0 - '@ethersproject/properties': 5.4.0 - '@ethersproject/transactions': 5.4.0 + '@ethersproject/abi': 5.7.0 + '@ethersproject/abstract-provider': 5.7.0 + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/transactions': 5.7.0 dev: true /@ethersproject/contracts@5.7.0: @@ -449,14 +503,14 @@ packages: /@ethersproject/hash@5.4.0: resolution: {integrity: sha512-xymAM9tmikKgbktOCjW60Z5sdouiIIurkZUr9oW5NOex5uwxrbsYG09kb5bMcNjlVeJD3yPivTNzViIs1GCbqA==} dependencies: - '@ethersproject/abstract-signer': 5.4.0 - '@ethersproject/address': 5.4.0 - '@ethersproject/bignumber': 5.4.0 - '@ethersproject/bytes': 5.4.0 - '@ethersproject/keccak256': 5.4.0 - '@ethersproject/logger': 5.4.0 - '@ethersproject/properties': 5.4.0 - '@ethersproject/strings': 5.4.0 + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/strings': 5.7.0 dev: true /@ethersproject/hash@5.7.0: @@ -476,18 +530,18 @@ packages: /@ethersproject/hdnode@5.4.0: resolution: {integrity: sha512-pKxdS0KAaeVGfZPp1KOiDLB0jba11tG6OP1u11QnYfb7pXn6IZx0xceqWRr6ygke8+Kw74IpOoSi7/DwANhy8Q==} dependencies: - '@ethersproject/abstract-signer': 5.4.0 - '@ethersproject/basex': 5.4.0 - '@ethersproject/bignumber': 5.4.0 - '@ethersproject/bytes': 5.4.0 - '@ethersproject/logger': 5.4.0 - '@ethersproject/pbkdf2': 5.4.0 - '@ethersproject/properties': 5.4.0 - '@ethersproject/sha2': 5.4.0 - '@ethersproject/signing-key': 5.4.0 - '@ethersproject/strings': 5.4.0 - '@ethersproject/transactions': 5.4.0 - '@ethersproject/wordlists': 5.4.0 + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/basex': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/pbkdf2': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/sha2': 5.7.0 + '@ethersproject/signing-key': 5.7.0 + '@ethersproject/strings': 5.7.0 + '@ethersproject/transactions': 5.7.0 + '@ethersproject/wordlists': 5.7.0 dev: true /@ethersproject/hdnode@5.7.0: @@ -510,17 +564,17 @@ packages: /@ethersproject/json-wallets@5.4.0: resolution: {integrity: sha512-igWcu3fx4aiczrzEHwG1xJZo9l1cFfQOWzTqwRw/xcvxTk58q4f9M7cjh51EKphMHvrJtcezJ1gf1q1AUOfEQQ==} dependencies: - '@ethersproject/abstract-signer': 5.4.0 - '@ethersproject/address': 5.4.0 - '@ethersproject/bytes': 5.4.0 - '@ethersproject/hdnode': 5.4.0 - '@ethersproject/keccak256': 5.4.0 - '@ethersproject/logger': 5.4.0 - '@ethersproject/pbkdf2': 5.4.0 - '@ethersproject/properties': 5.4.0 - '@ethersproject/random': 5.4.0 - '@ethersproject/strings': 5.4.0 - '@ethersproject/transactions': 5.4.0 + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/hdnode': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/pbkdf2': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/random': 5.7.0 + '@ethersproject/strings': 5.7.0 + '@ethersproject/transactions': 5.7.0 aes-js: 3.0.0 scrypt-js: 3.0.1 dev: true @@ -546,7 +600,7 @@ packages: /@ethersproject/keccak256@5.4.0: resolution: {integrity: sha512-FBI1plWet+dPUvAzPAeHzRKiPpETQzqSUWR1wXJGHVWi4i8bOSrpC3NwpkPjgeXG7MnugVc1B42VbfnQikyC/A==} dependencies: - '@ethersproject/bytes': 5.4.0 + '@ethersproject/bytes': 5.7.0 js-sha3: 0.5.7 dev: true @@ -568,7 +622,7 @@ packages: /@ethersproject/networks@5.4.0: resolution: {integrity: sha512-5fywtKRDcnaVeA5SjxXH3DOQqe/IbeD/plwydi94SdPps1fbDUrnO6SzDExaruBZXxpxJcO9upG9UComsei4bg==} dependencies: - '@ethersproject/logger': 5.4.0 + '@ethersproject/logger': 5.7.0 dev: true /@ethersproject/networks@5.7.1: @@ -580,8 +634,8 @@ packages: /@ethersproject/pbkdf2@5.4.0: resolution: {integrity: sha512-x94aIv6tiA04g6BnazZSLoRXqyusawRyZWlUhKip2jvoLpzJuLb//KtMM6PEovE47pMbW+Qe1uw+68ameJjB7g==} dependencies: - '@ethersproject/bytes': 5.4.0 - '@ethersproject/sha2': 5.4.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/sha2': 5.7.0 dev: true /@ethersproject/pbkdf2@5.7.0: @@ -594,7 +648,7 @@ packages: /@ethersproject/properties@5.4.0: resolution: {integrity: sha512-7jczalGVRAJ+XSRvNA6D5sAwT4gavLq3OXPuV/74o3Rd2wuzSL035IMpIMgei4CYyBdialJMrTqkOnzccLHn4A==} dependencies: - '@ethersproject/logger': 5.4.0 + '@ethersproject/logger': 5.7.0 dev: true /@ethersproject/properties@5.7.0: @@ -606,23 +660,23 @@ packages: /@ethersproject/providers@5.4.0: resolution: {integrity: sha512-XRmI9syLnkNdLA8ikEeg0duxmwSWTTt9S+xabnTOyI51JPJyhQ0QUNT+wvmod218ebb7rLupHDPQ7UVe2/+Tjg==} dependencies: - '@ethersproject/abstract-provider': 5.4.0 - '@ethersproject/abstract-signer': 5.4.0 - '@ethersproject/address': 5.4.0 - '@ethersproject/basex': 5.4.0 - '@ethersproject/bignumber': 5.4.0 - '@ethersproject/bytes': 5.4.0 - '@ethersproject/constants': 5.4.0 - '@ethersproject/hash': 5.4.0 - '@ethersproject/logger': 5.4.0 - '@ethersproject/networks': 5.4.0 - '@ethersproject/properties': 5.4.0 - '@ethersproject/random': 5.4.0 - '@ethersproject/rlp': 5.4.0 - '@ethersproject/sha2': 5.4.0 - '@ethersproject/strings': 5.4.0 - '@ethersproject/transactions': 5.4.0 - '@ethersproject/web': 5.4.0 + '@ethersproject/abstract-provider': 5.7.0 + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/basex': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/hash': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/networks': 5.7.1 + '@ethersproject/properties': 5.7.0 + '@ethersproject/random': 5.7.0 + '@ethersproject/rlp': 5.7.0 + '@ethersproject/sha2': 5.7.0 + '@ethersproject/strings': 5.7.0 + '@ethersproject/transactions': 5.7.0 + '@ethersproject/web': 5.7.1 bech32: 1.1.4 ws: 7.4.6 transitivePeerDependencies: @@ -661,8 +715,8 @@ packages: /@ethersproject/random@5.4.0: resolution: {integrity: sha512-pnpWNQlf0VAZDEOVp1rsYQosmv2o0ITS/PecNw+mS2/btF8eYdspkN0vIXrCMtkX09EAh9bdk8GoXmFXM1eAKw==} dependencies: - '@ethersproject/bytes': 5.4.0 - '@ethersproject/logger': 5.4.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 dev: true /@ethersproject/random@5.7.0: @@ -675,8 +729,8 @@ packages: /@ethersproject/rlp@5.4.0: resolution: {integrity: sha512-0I7MZKfi+T5+G8atId9QaQKHRvvasM/kqLyAH4XxBCBchAooH2EX5rL9kYZWwcm3awYV+XC7VF6nLhfeQFKVPg==} dependencies: - '@ethersproject/bytes': 5.4.0 - '@ethersproject/logger': 5.4.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 dev: true /@ethersproject/rlp@5.7.0: @@ -689,8 +743,8 @@ packages: /@ethersproject/sha2@5.4.0: resolution: {integrity: sha512-siheo36r1WD7Cy+bDdE1BJ8y0bDtqXCOxRMzPa4bV1TGt/eTUUt03BHoJNB6reWJD8A30E/pdJ8WFkq+/uz4Gg==} dependencies: - '@ethersproject/bytes': 5.4.0 - '@ethersproject/logger': 5.4.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 hash.js: 1.1.7 dev: true @@ -705,9 +759,9 @@ packages: /@ethersproject/signing-key@5.4.0: resolution: {integrity: sha512-q8POUeywx6AKg2/jX9qBYZIAmKSB4ubGXdQ88l40hmATj29JnG5pp331nAWwwxPn2Qao4JpWHNZsQN+bPiSW9A==} dependencies: - '@ethersproject/bytes': 5.4.0 - '@ethersproject/logger': 5.4.0 - '@ethersproject/properties': 5.4.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 bn.js: 4.12.0 elliptic: 6.5.4 hash.js: 1.1.7 @@ -727,11 +781,11 @@ packages: /@ethersproject/solidity@5.4.0: resolution: {integrity: sha512-XFQTZ7wFSHOhHcV1DpcWj7VXECEiSrBuv7JErJvB9Uo+KfCdc3QtUZV+Vjh/AAaYgezUEKbCtE6Khjm44seevQ==} dependencies: - '@ethersproject/bignumber': 5.4.0 - '@ethersproject/bytes': 5.4.0 - '@ethersproject/keccak256': 5.4.0 - '@ethersproject/sha2': 5.4.0 - '@ethersproject/strings': 5.4.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/sha2': 5.7.0 + '@ethersproject/strings': 5.7.0 dev: true /@ethersproject/solidity@5.7.0: @@ -748,9 +802,9 @@ packages: /@ethersproject/strings@5.4.0: resolution: {integrity: sha512-k/9DkH5UGDhv7aReXLluFG5ExurwtIpUfnDNhQA29w896Dw3i4uDTz01Quaptbks1Uj9kI8wo9tmW73wcIEaWA==} dependencies: - '@ethersproject/bytes': 5.4.0 - '@ethersproject/constants': 5.4.0 - '@ethersproject/logger': 5.4.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/logger': 5.7.0 dev: true /@ethersproject/strings@5.7.0: @@ -764,15 +818,15 @@ packages: /@ethersproject/transactions@5.4.0: resolution: {integrity: sha512-s3EjZZt7xa4BkLknJZ98QGoIza94rVjaEed0rzZ/jB9WrIuu/1+tjvYCWzVrystXtDswy7TPBeIepyXwSYa4WQ==} dependencies: - '@ethersproject/address': 5.4.0 - '@ethersproject/bignumber': 5.4.0 - '@ethersproject/bytes': 5.4.0 - '@ethersproject/constants': 5.4.0 - '@ethersproject/keccak256': 5.4.0 - '@ethersproject/logger': 5.4.0 - '@ethersproject/properties': 5.4.0 - '@ethersproject/rlp': 5.4.0 - '@ethersproject/signing-key': 5.4.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/rlp': 5.7.0 + '@ethersproject/signing-key': 5.7.0 dev: true /@ethersproject/transactions@5.7.0: @@ -792,9 +846,9 @@ packages: /@ethersproject/units@5.4.0: resolution: {integrity: sha512-Z88krX40KCp+JqPCP5oPv5p750g+uU6gopDYRTBGcDvOASh6qhiEYCRatuM/suC4S2XW9Zz90QI35MfSrTIaFg==} dependencies: - '@ethersproject/bignumber': 5.4.0 - '@ethersproject/constants': 5.4.0 - '@ethersproject/logger': 5.4.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/logger': 5.7.0 dev: true /@ethersproject/units@5.7.0: @@ -808,21 +862,21 @@ packages: /@ethersproject/wallet@5.4.0: resolution: {integrity: sha512-wU29majLjM6AjCjpat21mPPviG+EpK7wY1+jzKD0fg3ui5fgedf2zEu1RDgpfIMsfn8fJHJuzM4zXZ2+hSHaSQ==} dependencies: - '@ethersproject/abstract-provider': 5.4.0 - '@ethersproject/abstract-signer': 5.4.0 - '@ethersproject/address': 5.4.0 - '@ethersproject/bignumber': 5.4.0 - '@ethersproject/bytes': 5.4.0 - '@ethersproject/hash': 5.4.0 - '@ethersproject/hdnode': 5.4.0 - '@ethersproject/json-wallets': 5.4.0 - '@ethersproject/keccak256': 5.4.0 - '@ethersproject/logger': 5.4.0 - '@ethersproject/properties': 5.4.0 - '@ethersproject/random': 5.4.0 - '@ethersproject/signing-key': 5.4.0 - '@ethersproject/transactions': 5.4.0 - '@ethersproject/wordlists': 5.4.0 + '@ethersproject/abstract-provider': 5.7.0 + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/hash': 5.7.0 + '@ethersproject/hdnode': 5.7.0 + '@ethersproject/json-wallets': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/random': 5.7.0 + '@ethersproject/signing-key': 5.7.0 + '@ethersproject/transactions': 5.7.0 + '@ethersproject/wordlists': 5.7.0 dev: true /@ethersproject/wallet@5.7.0: @@ -848,11 +902,11 @@ packages: /@ethersproject/web@5.4.0: resolution: {integrity: sha512-1bUusGmcoRLYgMn6c1BLk1tOKUIFuTg8j+6N8lYlbMpDesnle+i3pGSagGNvwjaiLo4Y5gBibwctpPRmjrh4Og==} dependencies: - '@ethersproject/base64': 5.4.0 - '@ethersproject/bytes': 5.4.0 - '@ethersproject/logger': 5.4.0 - '@ethersproject/properties': 5.4.0 - '@ethersproject/strings': 5.4.0 + '@ethersproject/base64': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/strings': 5.7.0 dev: true /@ethersproject/web@5.7.1: @@ -868,11 +922,11 @@ packages: /@ethersproject/wordlists@5.4.0: resolution: {integrity: sha512-FemEkf6a+EBKEPxlzeVgUaVSodU7G0Na89jqKjmWMlDB0tomoU8RlEMgUvXyqtrg8N4cwpLh8nyRnm1Nay1isA==} dependencies: - '@ethersproject/bytes': 5.4.0 - '@ethersproject/hash': 5.4.0 - '@ethersproject/logger': 5.4.0 - '@ethersproject/properties': 5.4.0 - '@ethersproject/strings': 5.4.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/hash': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/strings': 5.7.0 dev: true /@ethersproject/wordlists@5.7.0: @@ -3784,7 +3838,7 @@ packages: resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} hasBin: true dependencies: - glob: 7.2.0 + glob: 7.2.3 dev: true /ripemd160@2.0.2: @@ -4456,6 +4510,12 @@ packages: ethers: 5.7.2 dev: true + github.com/daimo-eth/p256-verifier/29475ae300ec95d98d5c7cc34c094846f0aa2dcd: + resolution: {tarball: https://codeload.github.com/daimo-eth/p256-verifier/tar.gz/29475ae300ec95d98d5c7cc34c094846f0aa2dcd} + name: p256-verifier + version: 0.0.0 + dev: true + github.com/dapphub/ds-test/e282159d5170298eb2455a6c05280ab5a73a4ef0: resolution: {tarball: https://codeload.github.com/dapphub/ds-test/tar.gz/e282159d5170298eb2455a6c05280ab5a73a4ef0} name: ds-test @@ -4554,6 +4614,12 @@ packages: version: 1.7.6 dev: true + github.com/foundry-rs/forge-std/2f6762e4f73f3d835457c220b5f62dfeeb6f6341: + resolution: {tarball: https://codeload.github.com/foundry-rs/forge-std/tar.gz/2f6762e4f73f3d835457c220b5f62dfeeb6f6341} + name: forge-std + version: 1.7.6 + dev: true + github.com/kopy-kat/account-abstraction/396bda190ddc00919339986c72c580c160a15587(ethers@5.4.0)(hardhat@2.20.1)(lodash@4.17.21)(typechain@5.2.0): resolution: {tarball: https://codeload.github.com/kopy-kat/account-abstraction/tar.gz/396bda190ddc00919339986c72c580c160a15587} id: github.com/kopy-kat/account-abstraction/396bda190ddc00919339986c72c580c160a15587 @@ -4659,6 +4725,15 @@ packages: solady: github.com/vectorized/solady/21009ce09f02c0e20ce4750b63577e8c0cc7ced8 dev: true + github.com/rhinestonewtf/erc4337-validation/5bc7fa2dec4ed815d389c41df0c93151e1ff6cae: + resolution: {tarball: https://codeload.github.com/rhinestonewtf/erc4337-validation/tar.gz/5bc7fa2dec4ed815d389c41df0c93151e1ff6cae} + name: '@rhinestone/erc4337-validation' + version: 0.0.1-alpha + dependencies: + '@openzeppelin/contracts': 5.0.1 + solady: github.com/vectorized/solady/e7024bee47b1623f436ee491ca9458a6dc8abce9 + dev: true + github.com/sablier-labs/solarray/6bf10cb34cdace52a3ba5fe437e78cc82df92684: resolution: {tarball: https://codeload.github.com/sablier-labs/solarray/tar.gz/6bf10cb34cdace52a3ba5fe437e78cc82df92684} name: solarray @@ -4683,6 +4758,18 @@ packages: version: 0.0.170 dev: true + github.com/vectorized/solady/d699161248fdb571a35fe12f4bd3077032f33806: + resolution: {tarball: https://codeload.github.com/vectorized/solady/tar.gz/d699161248fdb571a35fe12f4bd3077032f33806} + name: solady + version: 0.0.172 + dev: true + + github.com/vectorized/solady/e7024bee47b1623f436ee491ca9458a6dc8abce9: + resolution: {tarball: https://codeload.github.com/vectorized/solady/tar.gz/e7024bee47b1623f436ee491ca9458a6dc8abce9} + name: solady + version: 0.0.173 + dev: true + github.com/zeroknots/sentinellist/5f851f29b5d5e0fd4f5cdc63a2ccd7865ab1802d: resolution: {tarball: https://codeload.github.com/zeroknots/sentinellist/tar.gz/5f851f29b5d5e0fd4f5cdc63a2ccd7865ab1802d} name: sentinellist From 1bcc9ef75d94e9813dac0fdb20e9e1f417588cee Mon Sep 17 00:00:00 2001 From: zeroknots Date: Mon, 4 Mar 2024 12:48:40 +0700 Subject: [PATCH 9/9] =?UTF-8?q?=E2=9A=B0=EF=B8=8F=20Removing=20console=20l?= =?UTF-8?q?ogs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/PermissionManager/src/PermissionValidator.sol | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/PermissionManager/src/PermissionValidator.sol b/packages/PermissionManager/src/PermissionValidator.sol index c74dc5d3..64e14089 100644 --- a/packages/PermissionManager/src/PermissionValidator.sol +++ b/packages/PermissionManager/src/PermissionValidator.sol @@ -206,8 +206,6 @@ contract PermissionValidator is ERC7579ValidatorBase { { require(_userOp.sender == msg.sender, "sender must be msg.sender"); bytes32 permissionId = bytes32(_userOp.signature[0:32]); - console2.log("validate user Op. PermissionID:"); - console2.logBytes32(permissionId); if ( permissions[permissionId][msg.sender].flag & toFlag(1) == toFlag(0) || nonces[msg.sender].revoked > permissions[permissionId][msg.sender].nonce @@ -217,8 +215,6 @@ contract PermissionValidator is ERC7579ValidatorBase { Permission memory permission = permissions[permissionId][msg.sender]; PolicyConfig policy = permission.firstPolicy; uint256 cursor = 32; - console2.log("policy", address(PolicyConfigLib.getAddress(policy))); - console2.logBytes32(PolicyConfig.unwrap(policy)); while (address(PolicyConfigLib.getAddress(policy)) != address(0)) { // if (PolicyConfigLib.skipOnValidateUserOp(policy)) { // policy = nextPolicy[permissionId][policy][msg.sender]; @@ -233,7 +229,6 @@ contract PermissionValidator is ERC7579ValidatorBase { && address(bytes20(_userOp.signature[cursor:cursor + 20])) == address(PolicyConfigLib.getAddress(policy)) ) { - console2.log("if"); // only when policy address is same as the one in signature uint256 length = uint256(bytes32(_userOp.signature[cursor + 20:cursor + 52])); require( @@ -244,12 +239,9 @@ contract PermissionValidator is ERC7579ValidatorBase { // policyDataLength, policyData] cursor += 52 + length; } else { - console2.log("else"); policyData = _userOp.signature[cursor:cursor]; } - console2.log("policyData"); - console2.logBytes(policyData); ValidationData policyValidation = PolicyConfigLib.getAddress(policy).checkUserOpPolicy( msg.sender, permissionId, _userOp, policyData );