diff --git a/.npmrc b/.npmrc index c483022..77a286f 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ -shamefully-hoist=true \ No newline at end of file +shamefully-hoist=true +package-manager-strict=false \ No newline at end of file diff --git a/package.json b/package.json index 1690b99..3219b69 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "solady": "github:vectorized/solady" }, "devDependencies": { - "solhint": "^5.0.1" + "solhint": "^5.0.3" }, "files": [ "src", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6bb9f05..4720a52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4256,4 +4256,4 @@ snapshots: zksync-web3@0.14.4(ethers@5.7.2): dependencies: - ethers: 5.7.2 + ethers: 5.7.2 \ No newline at end of file diff --git a/src/MSAAdvanced.sol b/src/MSAAdvanced.sol index 7eb6587..45a9324 100644 --- a/src/MSAAdvanced.sol +++ b/src/MSAAdvanced.sol @@ -15,6 +15,7 @@ import { HashLib } from "./lib/HashLib.sol"; import { ECDSA } from "solady/utils/ECDSA.sol"; import { Initializable } from "./lib/Initializable.sol"; import { ERC7779Adapter } from "./core/ERC7779Adapter.sol"; +import { SentinelListLib } from "sentinellist/SentinelList.sol"; /** * @author zeroknots.eth | rhinestone.wtf @@ -34,6 +35,7 @@ contract MSAAdvanced is using ExecutionLib for bytes; using ModeLib for ModeCode; using ECDSA for bytes32; + using SentinelListLib for SentinelListLib.SentinelList; /** * @inheritdoc IERC7579Account @@ -254,7 +256,6 @@ contract MSAAdvanced is if (signer != address(this)) { return VALIDATION_FAILED; } - return VALIDATION_SUCCESS; } else { return VALIDATION_FAILED; @@ -369,6 +370,18 @@ contract MSAAdvanced is // checks if already initialized and reverts before setting the state to initialized _initModuleManager(); + bool isERC7702; + assembly { + isERC7702 := + eq( + extcodehash(address()), + 0xeadcdba66a79ab5dce91622d1d75c8cff5cff0b96944c3bf1072cd08ce018329 // (keccak256(0xef01)) + ) + } + if (isERC7702) { + _addStorageBase(MODULEMANAGER_STORAGE_LOCATION); + _addStorageBase(HOOKMANAGER_STORAGE_LOCATION); + } // bootstrap the account (address bootstrap, bytes memory bootstrapCall) = abi.decode(data, (address, bytes)); @@ -386,4 +399,11 @@ contract MSAAdvanced is (bool success,) = bootstrap.delegatecall(bootstrapCall); if (!success) revert(); } + + function _onRedelegation() internal override { + _tryUninstallValidators(); + _tryUninstallExecutors(); + _tryUninstallHook(_getHook()); + _initModuleManager(); + } } diff --git a/src/core/ERC7779Adapter.sol b/src/core/ERC7779Adapter.sol index 1304fb3..b94a6b0 100644 --- a/src/core/ERC7779Adapter.sol +++ b/src/core/ERC7779Adapter.sol @@ -1,7 +1,19 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; +pragma solidity ^0.8.27; -abstract contract ERC7779Adapter { +import {IERC7779} from "../interfaces/IERC7779.sol"; + +abstract contract ERC7779Adapter is IERC7779 { + error NonAuthorizedOnRedelegationCaller(); + + // keccak256(abi.encode(uint256(keccak256(bytes("InteroperableDelegatedAccount.ERC.Storage"))) - + // 1)) & ~bytes32(uint256(0xff)); + bytes32 internal constant ERC7779_STORAGE_BASE = + 0xc473de86d0138e06e4d4918a106463a7cc005258d2e21915272bcb4594c18900; + + struct ERC7779Storage { + bytes32[] storageBases; + } /* * @dev Externally shares the storage bases that has been used throughout the account. * Majority of 7702 accounts will have their distinctive storage base to reduce the @@ -16,7 +28,22 @@ abstract contract ERC7779Adapter { storage at this slot, but just append their base to the array. * This append operation should be done during the initialization of the account. */ - function accountStorageBases() external view returns (bytes32[] memory) { } + + function accountStorageBases() external view returns (bytes32[] memory) { + ERC7779Storage storage $; + assembly { + $.slot := ERC7779_STORAGE_BASE + } + return $.storageBases; + } + + function _addStorageBase(bytes32 storageBase) internal { + ERC7779Storage storage $; + assembly { + $.slot := ERC7779_STORAGE_BASE + } + $.storageBases.push(storageBase); + } /* * @dev Function called before redelegation. @@ -28,9 +55,13 @@ abstract contract ERC7779Adapter { for redelegation. * msg.sender should be the owner of the account. */ - function onRedelegation() external pure returns (bool) { - // this is not implemented at the moment so that the account can preserve state across - // delegations + function onRedelegation() external returns (bool) { + require(msg.sender == address(this), NonAuthorizedOnRedelegationCaller()); + _onRedelegation(); return true; } + + /// @dev This function is called before redelegation. + /// @dev Account should override this function to implement the specific logic. + function _onRedelegation() internal virtual; } diff --git a/src/core/HookManager.sol b/src/core/HookManager.sol index 5f07f3e..3e203f2 100644 --- a/src/core/HookManager.sol +++ b/src/core/HookManager.sol @@ -10,6 +10,8 @@ import "../interfaces/IERC7579Module.sol"; * @author zeroknots.eth | rhinestone.wtf */ abstract contract HookManager { + event HookUninstallFailed(address hook, bytes data); + /// @custom:storage-location erc7201:hookmanager.storage.msa struct HookManagerStorage { IHook _hook; @@ -55,6 +57,13 @@ abstract contract HookManager { IHook(hook).onUninstall(data); } + function _tryUninstallHook(address hook) internal virtual { + _setHook(address(0)); + try IHook(hook).onUninstall("") {} catch { + emit HookUninstallFailed(hook, ""); + } + } + function _getHook() internal view returns (address _hook) { bytes32 slot = HOOKMANAGER_STORAGE_LOCATION; assembly { diff --git a/src/core/ModuleManager.sol b/src/core/ModuleManager.sol index 0d6ec9e..bb1c31d 100644 --- a/src/core/ModuleManager.sol +++ b/src/core/ModuleManager.sol @@ -24,6 +24,9 @@ abstract contract ModuleManager is AccountBase, Receiver { error NoFallbackHandler(bytes4 selector); error CannotRemoveLastValidator(); + event ValidatorUninstallFailed(address validator, bytes data); + event ExecutorUninstallFailed(address executor, bytes data); + // forgefmt: disable-next-line // keccak256(abi.encode(uint256(keccak256("modulemanager.storage.msa")) - 1)) & ~bytes32(uint256(0xff)); bytes32 internal constant MODULEMANAGER_STORAGE_LOCATION = @@ -92,6 +95,39 @@ abstract contract ModuleManager is AccountBase, Receiver { IValidator(validator).onUninstall(disableModuleData); } + /* + function _tryUninstallValidators(bytes[] calldata data) internal { + SentinelListLib.SentinelList storage $valdiators = $moduleManager().$valdiators; + uint256 length = data.length; + uint256 index; + address validator = $valdiators.getNext(SENTINEL); + while (validator != SENTINEL) { + bytes memory uninstallData; + if (index < length) { + uninstallData = data[index]; + } + try IValidator(validator).onUninstall(uninstallData) {} catch { + emit ValidatorUninstallFailed(validator, uninstallData); + } + validator = $valdiators.getNext(validator); + index++; + } + $valdiators.popAll(); + } + */ + + function _tryUninstallValidators() internal { + SentinelListLib.SentinelList storage $valdiators = $moduleManager().$valdiators; + address validator = $valdiators.getNext(SENTINEL); + while (validator != SENTINEL) { + try IValidator(validator).onUninstall("") {} catch { + emit ValidatorUninstallFailed(validator, ""); + } + validator = $valdiators.getNext(validator); + } + $valdiators.popAll(); + } + function _isValidatorInstalled(address validator) internal view virtual returns (bool) { SentinelListLib.SentinelList storage $valdiators = $moduleManager().$valdiators; return $valdiators.contains(validator); @@ -131,6 +167,39 @@ abstract contract ModuleManager is AccountBase, Receiver { IExecutor(executor).onUninstall(disableModuleData); } + /* + function _tryUninstallExecutors(bytes[] calldata data) internal { + SentinelListLib.SentinelList storage $executors = $moduleManager().$executors; + uint256 length = data.length; + uint256 index; + address executor = $executors.getNext(SENTINEL); + while (executor != SENTINEL) { + bytes memory uninstallData; + if (index < length) { + uninstallData = data[index]; + } + try IExecutor(executor).onUninstall(uninstallData) {} catch { + emit ExecutorUninstallFailed(executor, uninstallData); + } + executor = $executors.getNext(executor); + index++; + } + $executors.popAll(); + } + */ + + function _tryUninstallExecutors() internal { + SentinelListLib.SentinelList storage $executors = $moduleManager().$executors; + address executor = $executors.getNext(SENTINEL); + while (executor != SENTINEL) { + try IExecutor(executor).onUninstall("") {} catch { + emit ExecutorUninstallFailed(executor, ""); + } + executor = $executors.getNext(executor); + } + $executors.popAll(); + } + function _isExecutorInstalled(address executor) internal view virtual returns (bool) { SentinelListLib.SentinelList storage $executors = $moduleManager().$executors; return $executors.contains(executor); diff --git a/src/interfaces/IERC7779.sol b/src/interfaces/IERC7779.sol new file mode 100644 index 0000000..1b0a467 --- /dev/null +++ b/src/interfaces/IERC7779.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +interface IERC7779 { + /* + * @dev Externally shares the storage bases that has been used throughout the account. + * Majority of 7702 accounts will have their distinctive storage base to reduce the chance of storage collision. + * This allows the external entities to know what the storage base is of the account. + * Wallets willing to redelegate already-delegated accounts should call accountStorageBase() to check if it confirms with the account it plans to redelegate. + * + * The bytes32 array should be stored at the storage slot: keccak(keccak('InteroperableDelegatedAccount.ERC.Storage')-1) & ~0xff + * This is an append-only array so newly redelegated accounts should not overwrite the storage at this slot, but just append their base to the array. + * This append operation should be done during the initialization of the account. + */ + function accountStorageBases() external view returns (bytes32[] memory); + + /* + * @dev Function called before redelegation. + * This function should prepare the account for a delegation to a different implementation. + * This function could be triggered by the new wallet that wants to redelegate an already delegated EOA. + * It should uninitialize storages if needed and execute wallet-specific logic to prepare for redelegation. + * msg.sender should be the owner of the account. + */ + function onRedelegation() external returns (bool); + +} diff --git a/src/interfaces/IMSA.sol b/src/interfaces/IMSA.sol index 5766cbd..459a6fb 100644 --- a/src/interfaces/IMSA.sol +++ b/src/interfaces/IMSA.sol @@ -3,10 +3,11 @@ pragma solidity ^0.8.21; import { IERC7579Account } from "./IERC7579Account.sol"; import { IERC4337Account } from "./IERC4337Account.sol"; +import { IERC7779 } from "./IERC7779.sol"; import { CallType, ExecType, ModeCode } from "../lib/ModeLib.sol"; -interface IMSA is IERC7579Account, IERC4337Account { +interface IMSA is IERC7579Account, IERC4337Account, IERC7779 { // Error thrown when an unsupported ModuleType is requested error UnsupportedModuleType(uint256 moduleTypeId); // Error thrown when an execution with an unsupported CallType was made diff --git a/test/advanced/EIP7702.t.sol b/test/advanced/EIP7702.t.sol index 0070f81..2263ac1 100644 --- a/test/advanced/EIP7702.t.sol +++ b/test/advanced/EIP7702.t.sol @@ -19,6 +19,8 @@ import { import "./TestBaseUtilAdvanced.t.sol"; import { HashLib } from "src/lib/HashLib.sol"; import { ECDSA } from "solady/utils/ECDSA.sol"; +import { MODULE_TYPE_VALIDATOR, MODULE_TYPE_EXECUTOR, MODULE_TYPE_HOOK } from "src/core/ModuleManager.sol"; +import { MockHook } from "../mocks/MockHook.sol"; contract EIP7702 is TestBaseUtilAdvanced { using ECDSA for bytes32; @@ -284,4 +286,34 @@ contract EIP7702 is TestBaseUtilAdvanced { // Assert that the value was set ie that execution was successful assertTrue(valueTarget.balance == value); } + + function test_onRedelegation() public { + address account = test_initializeAndExecSingle(); + + MockHook hook = new MockHook(); + + vm.prank(address(entrypoint)); + IMSA(account).installModule(MODULE_TYPE_HOOK, address(hook), ""); + + assertTrue(IMSA(account).isModuleInstalled(MODULE_TYPE_VALIDATOR, address(defaultValidator), "")); + assertTrue(IMSA(account).isModuleInstalled(MODULE_TYPE_EXECUTOR, address(defaultExecutor), "")); + assertTrue(IMSA(account).isModuleInstalled(MODULE_TYPE_HOOK, address(hook), "")); + // storage is cleared + vm.prank(address(account)); + IMSA(account).onRedelegation(); + assertFalse(IMSA(account).isModuleInstalled(MODULE_TYPE_VALIDATOR, address(defaultValidator), "")); + assertFalse(IMSA(account).isModuleInstalled(MODULE_TYPE_EXECUTOR, address(defaultExecutor), "")); + assertFalse(IMSA(account).isModuleInstalled(MODULE_TYPE_HOOK, address(hook), "")); + + // account is properly initialized to install modules again + vm.startPrank(address(entrypoint)); + IMSA(account).installModule(MODULE_TYPE_VALIDATOR, address(defaultValidator), ""); + IMSA(account).installModule(MODULE_TYPE_EXECUTOR, address(defaultExecutor), ""); + IMSA(account).installModule(MODULE_TYPE_HOOK, address(hook), ""); + + vm.stopPrank(); + assertTrue(IMSA(account).isModuleInstalled(MODULE_TYPE_VALIDATOR, address(defaultValidator), "")); + assertTrue(IMSA(account).isModuleInstalled(MODULE_TYPE_EXECUTOR, address(defaultExecutor), "")); + assertTrue(IMSA(account).isModuleInstalled(MODULE_TYPE_HOOK, address(hook), "")); + } } diff --git a/test/core/TestFuzz_ERC7779Adapter.t.sol b/test/core/TestFuzz_ERC7779Adapter.t.sol new file mode 100644 index 0000000..934a407 --- /dev/null +++ b/test/core/TestFuzz_ERC7779Adapter.t.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { MockERC7779 } from "../mocks/MockERC7779.sol"; +import "forge-std/Test.sol"; + +/// @title TestFuzz_ERC7779Adapter +/// @notice Tests the ERC7779Adapter contract +contract TestFuzz_ERC7779Adapter is Test { + MockERC7779 private mockERC7779; + + function setUp() public { + mockERC7779 = new MockERC7779(); + //bytes32 erc7779StorageBase = + // keccak256(abi.encode(uint256(keccak256(bytes("InteroperableDelegatedAccount.ERC.Storage"))) + // - 1)) & ~bytes32(uint256(0xff)); + //console.logBytes32(erc7779StorageBase); + } + + function test_Fuzz_ERC7779Adapter_AddStorageBases(uint256 amountOfBases) public { + vm.assume(amountOfBases > 0 && amountOfBases < 100); + bytes32[] memory expectedStorageBases = new bytes32[](amountOfBases); + + for (uint256 i = 0; i < amountOfBases; i++) { + bytes32 storageBase = bytes32(uint256(i)); + expectedStorageBases[i] = storageBase; + mockERC7779.addStorageBase(storageBase); + } + + bytes32[] memory retrievedStorageBases = mockERC7779.accountStorageBases(); + assertEq(retrievedStorageBases.length, amountOfBases); + for (uint256 i = 0; i < amountOfBases; i++) { + assertEq(retrievedStorageBases[i], expectedStorageBases[i]); + } + } +} diff --git a/test/mocks/MockERC7779.sol b/test/mocks/MockERC7779.sol new file mode 100644 index 0000000..fc89ca6 --- /dev/null +++ b/test/mocks/MockERC7779.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { ERC7779Adapter } from "src/core/ERC7779Adapter.sol"; + +contract MockERC7779 is ERC7779Adapter { + function addStorageBase(bytes32 storageBase) external { + _addStorageBase(storageBase); + } + + function _onRedelegation() internal override { + // do nothing + } +}