diff --git a/README.md b/README.md index d83dd0fe..b0162108 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,17 @@ Alchemy's Modular Account is a maximally modular, upgradeable Smart Contract Acc ## Overview This repository contains: -* [Modular Account implementation](src/account) -* [Modular Account factories](src/factory) -* 3 ERC-6900 compatible plugins: - * [MultiOwnerPlugin](src/plugins/owner) is a plugin supporting 1+ ECDSA owners. - * [TokenReceiverPlugin](src/plugins/TokenReceiverPlugin.sol) contains ERC721/ERC777/ERC1155 token receivers. - * [SessionKeyPlugin](src/plugins/session) enables session keys with optional permissions such as time ranges, token spend limits, and gas spend limits. + +- [Modular Account implementation](src/account) +- [Modular Account factory](src/factory) +- 2 ERC-6900 compatible plugins: + - [MultiOwnerPlugin](src/plugins/owner) is a plugin supporting 1+ ECDSA owners. + - [SessionKeyPlugin](src/plugins/session) enables session keys with optional permissions such as time ranges, token spend limits, and gas spend limits. The account and plugins conform to these ERC versions: -* ERC-4337: 0.6.0 -* ERC-6900: 0.7.0 + +- ERC-4337: 0.6.0 +- ERC-6900: 0.7.0 ## Development @@ -43,6 +44,7 @@ FOUNDRY_PROFILE=lite forge test -vvv ### Deployment A deployment script can be found in the `scripts/` folder + ```bash forge script script/Deploy.s.sol --rpc-url $RPC_URL --broadcast ``` diff --git a/foundry.toml b/foundry.toml index 5471c5a4..92af0449 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,7 +7,7 @@ out = 'out' test = 'test' libs = ['lib'] optimizer = true -optimizer_runs = 1600 +optimizer_runs = 900 ignored_error_codes = [] [fuzz] diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index b1c3cb96..4a27565f 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -17,21 +17,16 @@ pragma solidity ^0.8.22; -import {console} from "forge-std/Test.sol"; import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/Test.sol"; import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntryPoint.sol"; -import {IEntryPoint as IMSCAEntryPoint} from "../src/interfaces/erc4337/IEntryPoint.sol"; - import {UpgradeableModularAccount} from "../src/account/UpgradeableModularAccount.sol"; - import {MultiOwnerMSCAFactory} from "../src/factory/MultiOwnerMSCAFactory.sol"; -import {MultiOwnerTokenReceiverMSCAFactory} from "../src/factory/MultiOwnerTokenReceiverMSCAFactory.sol"; - +import {IEntryPoint as IMSCAEntryPoint} from "../src/interfaces/erc4337/IEntryPoint.sol"; import {BasePlugin} from "../src/plugins/BasePlugin.sol"; import {MultiOwnerPlugin} from "../src/plugins/owner/MultiOwnerPlugin.sol"; -import {TokenReceiverPlugin} from "../src/plugins/TokenReceiverPlugin.sol"; import {SessionKeyPlugin} from "../src/plugins/session/SessionKeyPlugin.sol"; contract Deploy is Script { @@ -47,13 +42,10 @@ contract Deploy is Script { address public ownerFactoryAddr = vm.envOr("OWNER_FACTORY", address(0)); address public ownerAndTokenReceiverFactoryAddr = vm.envOr("OWNER_TOKEN_RECEIVER_FACTORY", address(0)); MultiOwnerMSCAFactory ownerFactory; - MultiOwnerTokenReceiverMSCAFactory ownerAndTokenReceiverFactory; // Load plugins contract, if not in env, deploy new contract address public multiOwnerPlugin = vm.envOr("OWNER_PLUGIN", address(0)); bytes32 public multiOwnerPluginManifestHash; - address public tokenReceiverPlugin = vm.envOr("TOKEN_RECEIVER_PLUGIN", address(0)); - bytes32 public tokenReceiverPluginManifestHash; address public sessionKeyPlugin = vm.envOr("SESSION_KEY_PLUGIN", address(0)); function run() public { @@ -84,49 +76,19 @@ contract Deploy is Script { } multiOwnerPluginManifestHash = keccak256(abi.encode(BasePlugin(multiOwnerPlugin).pluginManifest())); - // Deploy multi owner plugin, and set plugin hash - if (tokenReceiverPlugin == address(0)) { - TokenReceiverPlugin trp = new TokenReceiverPlugin(); - tokenReceiverPlugin = address(trp); - console.log("New TokenReceiverPlugin: ", tokenReceiverPlugin); - } else { - console.log("Exist TokenReceiverPlugin: ", tokenReceiverPlugin); - } - tokenReceiverPluginManifestHash = keccak256(abi.encode(BasePlugin(tokenReceiverPlugin).pluginManifest())); - // Deploy MultiOwnerMSCAFactory, and add stake with EP - { - if (ownerFactoryAddr == address(0)) { - ownerFactory = new MultiOwnerMSCAFactory( - owner, multiOwnerPlugin, mscaImpl, multiOwnerPluginManifestHash, entryPoint - ); - - ownerFactoryAddr = address(ownerFactory); - console.log("New MultiOwnerMSCAFactory: ", ownerFactoryAddr); - } else { - console.log("Exist MultiOwnerMSCAFactory: ", ownerFactoryAddr); - } - _addStakeForFactory(ownerFactoryAddr, entryPoint); - } - // Deploy MultiOwnerTokenReceiverMSCAFactory, and add stake with EP - if (ownerAndTokenReceiverFactoryAddr == address(0)) { - ownerAndTokenReceiverFactory = new MultiOwnerTokenReceiverMSCAFactory( - owner, - multiOwnerPlugin, - tokenReceiverPlugin, - mscaImpl, - multiOwnerPluginManifestHash, - tokenReceiverPluginManifestHash, - entryPoint + if (ownerFactoryAddr == address(0)) { + ownerFactory = new MultiOwnerMSCAFactory( + owner, multiOwnerPlugin, mscaImpl, multiOwnerPluginManifestHash, entryPoint ); - ownerAndTokenReceiverFactoryAddr = address(ownerAndTokenReceiverFactory); - console.log("New MultiOwnerTokenReceiverMSCAFactory: ", ownerAndTokenReceiverFactoryAddr); + ownerFactoryAddr = address(ownerFactory); + console.log("New MultiOwnerMSCAFactory: ", ownerFactoryAddr); } else { - console.log("Exist MultiOwnerTokenReceiverMSCAFactory: ", ownerAndTokenReceiverFactoryAddr); + console.log("Exist MultiOwnerMSCAFactory: ", ownerFactoryAddr); } - _addStakeForFactory(ownerAndTokenReceiverFactoryAddr, entryPoint); + _addStakeForFactory(ownerFactoryAddr, entryPoint); // Deploy SessionKeyPlugin impl if (sessionKeyPlugin == address(0)) { diff --git a/src/account/PluginManagerInternals.sol b/src/account/PluginManagerInternals.sol index 751567eb..9c4e4f50 100644 --- a/src/account/PluginManagerInternals.sol +++ b/src/account/PluginManagerInternals.sol @@ -53,6 +53,9 @@ abstract contract PluginManagerInternals is IPluginManager, AccountStorageV1 { uint256 callbackGasLimit; } + // As per the EIP-165 spec, no interface should ever match 0xffffffff + bytes4 internal constant _INVALID_INTERFACE_ID = 0xffffffff; + // These flags are used in LinkedListSet values to optimize lookups. // It's important that they don't overlap with bit 1 and bit 2, which are reserved bits used to indicate // the sentinel value and the existence of a next value, respectively. @@ -64,10 +67,10 @@ abstract contract PluginManagerInternals is IPluginManager, AccountStorageV1 { error DuplicatePreUserOpValidationHookLimitExceeded(bytes4 selector, FunctionReference hook); error Erc4337FunctionNotAllowed(bytes4 selector); error ExecutionFunctionAlreadySet(bytes4 selector); - error IPluginFunctionNotAllowed(bytes4 selector); - error IPluginInterfaceNotAllowed(); + error InterfaceNotAllowed(); error InvalidDependenciesProvided(); error InvalidPluginManifest(); + error IPluginFunctionNotAllowed(bytes4 selector); error MissingPluginDependency(address dependency); error NativeFunctionNotAllowed(bytes4 selector); error NullFunctionReference(); @@ -452,8 +455,8 @@ abstract contract PluginManagerInternals is IPluginManager, AccountStorageV1 { length = manifest.interfaceIds.length; for (uint256 i = 0; i < length; ++i) { bytes4 interfaceId = manifest.interfaceIds[i]; - if (interfaceId == type(IPlugin).interfaceId) { - revert IPluginInterfaceNotAllowed(); + if (interfaceId == type(IPlugin).interfaceId || interfaceId == _INVALID_INTERFACE_ID) { + revert InterfaceNotAllowed(); } storage_.supportedInterfaces[interfaceId] += 1; } diff --git a/src/account/UpgradeableModularAccount.sol b/src/account/UpgradeableModularAccount.sol index dfabe633..23994701 100644 --- a/src/account/UpgradeableModularAccount.sol +++ b/src/account/UpgradeableModularAccount.sol @@ -17,6 +17,9 @@ pragma solidity ^0.8.22; +import {IERC1155Receiver} from "@openzeppelin/contracts/interfaces/IERC1155Receiver.sol"; +import {IERC777Recipient} from "@openzeppelin/contracts/interfaces/IERC777Recipient.sol"; +import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {UUPSUpgradeable} from "../../ext/UUPSUpgradeable.sol"; @@ -51,6 +54,9 @@ contract UpgradeableModularAccount is IAccountInitializable, IAccountView, IERC165, + IERC721Receiver, + IERC777Recipient, + IERC1155Receiver, IPluginExecutor, IStandardExecutor, UUPSUpgradeable @@ -80,8 +86,6 @@ contract UpgradeableModularAccount is // ERC-4337 v0.6.0 entrypoint address only IEntryPoint private immutable _ENTRY_POINT; - // As per the EIP-165 spec, no interface should ever match 0xffffffff - bytes4 internal constant _INTERFACE_ID_INVALID = 0xffffffff; bytes4 internal constant _IERC165_INTERFACE_ID = 0x01ffc9a7; event ModularAccountInitialized(IEntryPoint indexed entryPoint); @@ -370,16 +374,58 @@ contract UpgradeableModularAccount is _postNativeFunction(postExecHooks, postHookArgs); } - /// @inheritdoc IERC165 - function supportsInterface(bytes4 interfaceId) external view override returns (bool) { - if (interfaceId == _INTERFACE_ID_INVALID) { - return false; - } - if (interfaceId == _IERC165_INTERFACE_ID) { - return true; - } + /// @inheritdoc IERC777Recipient + /// @dev Runtime validation is bypassed for this function, but we still allow pre and post exec hooks to be + /// assigned and run. + function tokensReceived(address, address, address, uint256, bytes calldata, bytes calldata) + external + override + { + (FunctionReference[][] memory postExecHooks, bytes[] memory postHookArgs) = + _doPreExecHooks(_getAccountStorage().selectorData[msg.sig], ""); + _postNativeFunction(postExecHooks, postHookArgs); + } - return _getAccountStorage().supportedInterfaces[interfaceId] > 0; + /// @inheritdoc IERC721Receiver + /// @dev Runtime validation is bypassed for this function, but we still allow pre and post exec hooks to be + /// assigned and run. + function onERC721Received(address, address, uint256, bytes calldata) + external + override + returns (bytes4 selector) + { + (FunctionReference[][] memory postExecHooks, bytes[] memory postHookArgs) = + _doPreExecHooks(_getAccountStorage().selectorData[msg.sig], ""); + selector = IERC721Receiver.onERC721Received.selector; + _postNativeFunction(postExecHooks, postHookArgs); + } + + /// @inheritdoc IERC1155Receiver + /// @dev Runtime validation is bypassed for this function, but we still allow pre and post exec hooks to be + /// assigned and run. + function onERC1155Received(address, address, uint256, uint256, bytes calldata) + external + override + returns (bytes4 selector) + { + (FunctionReference[][] memory postExecHooks, bytes[] memory postHookArgs) = + _doPreExecHooks(_getAccountStorage().selectorData[msg.sig], ""); + selector = IERC1155Receiver.onERC1155Received.selector; + _postNativeFunction(postExecHooks, postHookArgs); + } + + /// @inheritdoc IERC1155Receiver + /// @dev Runtime validation is bypassed for this function, but we still allow pre and post exec hooks to be + /// assigned and run. + function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) + external + override + returns (bytes4 selector) + { + (FunctionReference[][] memory postExecHooks, bytes[] memory postHookArgs) = + _doPreExecHooks(_getAccountStorage().selectorData[msg.sig], ""); + selector = IERC1155Receiver.onERC1155BatchReceived.selector; + _postNativeFunction(postExecHooks, postHookArgs); } /// @inheritdoc UUPSUpgradeable @@ -389,6 +435,16 @@ contract UpgradeableModularAccount is _postNativeFunction(postExecHooks, postHookArgs); } + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + if (interfaceId == _INVALID_INTERFACE_ID) { + return false; + } + return interfaceId == _IERC165_INTERFACE_ID || interfaceId == type(IERC721Receiver).interfaceId + || interfaceId == type(IERC777Recipient).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId + || _getAccountStorage().supportedInterfaces[interfaceId] > 0; + } + /// @inheritdoc IAccountView function entryPoint() public view override returns (IEntryPoint) { return _ENTRY_POINT; @@ -417,7 +473,7 @@ contract UpgradeableModularAccount is } /// @dev Wraps execution of a native function with runtime validation and hooks. Used for upgradeToAndCall, - /// execute, executeBatch, installPlugin, uninstallPlugin. + /// execute, executeBatch, installPlugin, uninstallPlugin, and the token receiver functions. function _postNativeFunction(FunctionReference[][] memory postExecHooks, bytes[] memory postHookArgs) internal { diff --git a/src/factory/MultiOwnerTokenReceiverMSCAFactory.sol b/src/factory/MultiOwnerTokenReceiverMSCAFactory.sol deleted file mode 100644 index 7fb6acb8..00000000 --- a/src/factory/MultiOwnerTokenReceiverMSCAFactory.sol +++ /dev/null @@ -1,175 +0,0 @@ -// This file is part of Modular Account. -// -// Copyright 2024 Alchemy Insights, Inc. -// -// SPDX-License-Identifier: GPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General -// Public License as published by the Free Software Foundation, either version 3 of the License, or (at your -// option) any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the -// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along with this program. If not, see -// . - -pragma solidity ^0.8.22; - -import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; - -import {FactoryHelpers} from "../helpers/FactoryHelpers.sol"; -import {IEntryPoint} from "../interfaces/erc4337/IEntryPoint.sol"; -import {IAccountInitializable} from "../interfaces/IAccountInitializable.sol"; - -/// @title Multi Owner Plugin + Token Receiver MSCA (Modular Smart Contract Account) Factory -/// @author Alchemy -/// @notice Factory for upgradeable modular accounts with MultiOwnerPlugin and TokenReceiver installed. -/// @dev There is a reliance on the assumption that the plugin manifest will remain static, following ERC-6900. If -/// this assumption is broken then account deployments would be bricked. -contract MultiOwnerTokenReceiverMSCAFactory is Ownable2Step { - IEntryPoint public immutable ENTRYPOINT; - address public immutable MULTI_OWNER_PLUGIN; - address public immutable TOKEN_RECEIVER_PLUGIN; - address public immutable IMPL; - bytes32 internal immutable _MULTI_OWNER_PLUGIN_MANIFEST_HASH; - bytes32 internal immutable _TOKEN_RECEIVER_PLUGIN_MANIFEST_HASH; - uint256 internal constant _MAX_OWNERS_ON_CREATION = 100; - - error InvalidAction(); - error InvalidOwner(); - error OwnersArrayEmpty(); - error OwnersLimitExceeded(); - error TransferFailed(); - - /// @notice Constructor for the factory - constructor( - address owner, - address multiOwnerPlugin, - address tokenReceiverPlugin, - address implementation, - bytes32 multiOwnerPluginManifestHash, - bytes32 tokenReceiverPluginManifestHash, - IEntryPoint entryPoint - ) { - _transferOwnership(owner); - MULTI_OWNER_PLUGIN = multiOwnerPlugin; - TOKEN_RECEIVER_PLUGIN = tokenReceiverPlugin; - IMPL = implementation; - _MULTI_OWNER_PLUGIN_MANIFEST_HASH = multiOwnerPluginManifestHash; - _TOKEN_RECEIVER_PLUGIN_MANIFEST_HASH = tokenReceiverPluginManifestHash; - ENTRYPOINT = entryPoint; - } - - /// @notice Allow contract to receive native currency - receive() external payable {} - - /// @notice Create a modular smart contract account - /// @dev Account address depends on salt, impl addr, plugins and plugin init data - /// @dev The owner array must be in strictly ascending order and not include the 0 address. - /// @param salt salt for additional entropy for create2 - /// @param owners address array of the owners - function createAccount(uint256 salt, address[] calldata owners) external returns (address addr) { - if (!FactoryHelpers.isValidOwnerArray(owners)) { - revert InvalidOwner(); - } - - bytes[] memory pluginInitBytes = new bytes[](2); // empty bytes for TokenReceiverPlugin init - pluginInitBytes[0] = abi.encode(owners); - - bytes32 combinedSalt = FactoryHelpers.getCombinedSalt(salt, pluginInitBytes[0]); - addr = Create2.computeAddress( - combinedSalt, keccak256(abi.encodePacked(type(ERC1967Proxy).creationCode, abi.encode(IMPL, ""))) - ); - - // short circuit if exists - if (addr.code.length == 0) { - // not necessary to check return addr of this arg since next call fails if so - new ERC1967Proxy{salt: combinedSalt}(IMPL, ""); - - address[] memory plugins = new address[](2); - plugins[0] = MULTI_OWNER_PLUGIN; - plugins[1] = TOKEN_RECEIVER_PLUGIN; - - bytes32[] memory manifestHashes = new bytes32[](2); - manifestHashes[0] = _MULTI_OWNER_PLUGIN_MANIFEST_HASH; - manifestHashes[1] = _TOKEN_RECEIVER_PLUGIN_MANIFEST_HASH; - - IAccountInitializable(addr).initialize(plugins, abi.encode(manifestHashes, pluginInitBytes)); - } - } - - /// @notice Add stake to an entry point - /// @dev only callable by owner - /// @param unstakeDelay unstake delay for the stake - /// @param amount amount of native currency to stake - function addStake(uint32 unstakeDelay, uint256 amount) external payable onlyOwner { - ENTRYPOINT.addStake{value: amount}(unstakeDelay); - } - - /// @notice Start unlocking stake for an entry point - /// @dev only callable by owner - function unlockStake() external onlyOwner { - ENTRYPOINT.unlockStake(); - } - - /// @notice Withdraw stake from an entry point - /// @dev only callable by owner - /// @param to address to send native currency to - function withdrawStake(address payable to) external onlyOwner { - ENTRYPOINT.withdrawStake(to); - } - - /// @notice Withdraw funds from this contract - /// @dev can withdraw stuck erc20s or native currency - /// @param to address to send erc20s or native currency to - /// @param token address of the token to withdraw, 0 address for native currency - /// @param amount amount of the token to withdraw in case of rebasing tokens - function withdraw(address payable to, address token, uint256 amount) external onlyOwner { - if (token == address(0)) { - (bool success,) = to.call{value: address(this).balance}(""); - if (!success) { - revert TransferFailed(); - } - } else { - SafeERC20.safeTransfer(IERC20(token), to, amount); - } - } - - /// @notice Getter for counterfactual address based on input params - /// @dev The owner array must be in strictly ascending order and not include the 0 address. - /// @param salt salt for additional entropy for create2 - /// @param owners array of addresses of the owner - /// @return address of counterfactual account - function getAddress(uint256 salt, address[] calldata owners) external view returns (address) { - // Array can't be empty. - if (owners.length == 0) { - revert OwnersArrayEmpty(); - } - - // This protects against counterfactuals being generated against an exceptionally large number of owners - // that may exceed the block gas limit when actually creating the account. - if (owners.length > _MAX_OWNERS_ON_CREATION) { - revert OwnersLimitExceeded(); - } - - if (!FactoryHelpers.isValidOwnerArray(owners)) { - revert InvalidOwner(); - } - - return Create2.computeAddress( - FactoryHelpers.getCombinedSalt(salt, abi.encode(owners)), - keccak256(abi.encodePacked(type(ERC1967Proxy).creationCode, abi.encode(IMPL, ""))) - ); - } - - /// @notice Overriding to disable renounce ownership in Ownable - function renounceOwnership() public override onlyOwner { - revert InvalidAction(); - } -} diff --git a/src/helpers/KnownSelectors.sol b/src/helpers/KnownSelectors.sol index 2c8cb77a..ba677486 100644 --- a/src/helpers/KnownSelectors.sol +++ b/src/helpers/KnownSelectors.sol @@ -18,6 +18,9 @@ pragma solidity ^0.8.22; import {UUPSUpgradeable} from "../../ext/UUPSUpgradeable.sol"; +import {IERC1155Receiver} from "@openzeppelin/contracts/interfaces/IERC1155Receiver.sol"; +import {IERC777Recipient} from "@openzeppelin/contracts/interfaces/IERC777Recipient.sol"; +import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {IAccount} from "../../src/interfaces/erc4337/IAccount.sol"; @@ -60,7 +63,12 @@ library KnownSelectors { || selector == IAccountLoupe.getExecutionFunctionConfig.selector || selector == IAccountLoupe.getExecutionHooks.selector || selector == IAccountLoupe.getPreValidationHooks.selector - || selector == IAccountLoupe.getInstalledPlugins.selector; + || selector == IAccountLoupe.getInstalledPlugins.selector + // check against token receiver methods + || selector == IERC777Recipient.tokensReceived.selector + || selector == IERC721Receiver.onERC721Received.selector + || selector == IERC1155Receiver.onERC1155Received.selector + || selector == IERC1155Receiver.onERC1155BatchReceived.selector; } function isErc4337Function(bytes4 selector) internal pure returns (bool) { diff --git a/src/plugins/TokenReceiverPlugin.sol b/src/plugins/TokenReceiverPlugin.sol deleted file mode 100644 index 9c4654c0..00000000 --- a/src/plugins/TokenReceiverPlugin.sol +++ /dev/null @@ -1,137 +0,0 @@ -// This file is part of Modular Account. -// -// Copyright 2024 Alchemy Insights, Inc. -// -// SPDX-License-Identifier: GPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General -// Public License as published by the Free Software Foundation, either version 3 of the License, or (at your -// option) any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the -// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along with this program. If not, see -// . - -pragma solidity ^0.8.22; - -import {IERC1155Receiver} from "@openzeppelin/contracts/interfaces/IERC1155Receiver.sol"; -import {IERC777Recipient} from "@openzeppelin/contracts/interfaces/IERC777Recipient.sol"; -import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; - -import { - ManifestFunction, - ManifestAssociatedFunctionType, - ManifestAssociatedFunction, - PluginManifest, - PluginMetadata -} from "../interfaces/IPlugin.sol"; -import {BasePlugin} from "./BasePlugin.sol"; - -/// @title Token Receiver Plugin -/// @author Alchemy -/// @notice This plugin allows modular accounts to receive various types of tokens by implementing -/// required token receiver interfaces. -contract TokenReceiverPlugin is BasePlugin, IERC721Receiver, IERC777Recipient, IERC1155Receiver { - string internal constant _NAME = "Token Receiver Plugin"; - string internal constant _VERSION = "1.0.0"; - string internal constant _AUTHOR = "Alchemy"; - - // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - // ┃ Execution functions ┃ - // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - - function tokensReceived(address, address, address, uint256, bytes calldata, bytes calldata) - external - pure - override - // solhint-disable-next-line no-empty-blocks - {} - - function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) { - return IERC721Receiver.onERC721Received.selector; - } - - function onERC1155Received(address, address, uint256, uint256, bytes calldata) - external - pure - override - returns (bytes4) - { - return IERC1155Receiver.onERC1155Received.selector; - } - - function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) - external - pure - override - returns (bytes4) - { - return IERC1155Receiver.onERC1155BatchReceived.selector; - } - - // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - // ┃ Plugin interface functions ┃ - // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - - /// @inheritdoc BasePlugin - // solhint-disable-next-line no-empty-blocks - function onInstall(bytes calldata) external pure override {} - - /// @inheritdoc BasePlugin - // solhint-disable-next-line no-empty-blocks - function onUninstall(bytes calldata) external pure override {} - - /// @inheritdoc BasePlugin - function pluginManifest() external pure override returns (PluginManifest memory) { - PluginManifest memory manifest; - - manifest.executionFunctions = new bytes4[](4); - manifest.executionFunctions[0] = this.tokensReceived.selector; - manifest.executionFunctions[1] = this.onERC721Received.selector; - manifest.executionFunctions[2] = this.onERC1155Received.selector; - manifest.executionFunctions[3] = this.onERC1155BatchReceived.selector; - - // Only runtime validationFunction is needed since callbacks come from token contracts only - ManifestFunction memory alwaysAllowFunction = ManifestFunction({ - functionType: ManifestAssociatedFunctionType.RUNTIME_VALIDATION_ALWAYS_ALLOW, - functionId: 0, // Unused. - dependencyIndex: 0 // Unused. - }); - manifest.runtimeValidationFunctions = new ManifestAssociatedFunction[](4); - manifest.runtimeValidationFunctions[0] = ManifestAssociatedFunction({ - executionSelector: this.tokensReceived.selector, - associatedFunction: alwaysAllowFunction - }); - manifest.runtimeValidationFunctions[1] = ManifestAssociatedFunction({ - executionSelector: this.onERC721Received.selector, - associatedFunction: alwaysAllowFunction - }); - manifest.runtimeValidationFunctions[2] = ManifestAssociatedFunction({ - executionSelector: this.onERC1155Received.selector, - associatedFunction: alwaysAllowFunction - }); - manifest.runtimeValidationFunctions[3] = ManifestAssociatedFunction({ - executionSelector: this.onERC1155BatchReceived.selector, - associatedFunction: alwaysAllowFunction - }); - - manifest.interfaceIds = new bytes4[](3); - manifest.interfaceIds[0] = type(IERC721Receiver).interfaceId; - manifest.interfaceIds[1] = type(IERC777Recipient).interfaceId; - manifest.interfaceIds[2] = type(IERC1155Receiver).interfaceId; - - return manifest; - } - - /// @inheritdoc BasePlugin - function pluginMetadata() external pure virtual override returns (PluginMetadata memory) { - PluginMetadata memory metadata; - metadata.name = _NAME; - metadata.version = _VERSION; - metadata.author = _AUTHOR; - return metadata; - } -} diff --git a/test/plugin/TokenReceiverPlugin.t.sol b/test/account/TokenReceiver.t.sol similarity index 71% rename from test/plugin/TokenReceiverPlugin.t.sol rename to test/account/TokenReceiver.t.sol index 2415609a..f5c0c06d 100644 --- a/test/plugin/TokenReceiverPlugin.t.sol +++ b/test/account/TokenReceiver.t.sol @@ -26,19 +26,15 @@ import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Re import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import {IERC777Recipient} from "@openzeppelin/contracts/token/ERC777/IERC777Recipient.sol"; -import {AccountStorageV1} from "../../src/account/AccountStorageV1.sol"; import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; import {MultiOwnerMSCAFactory} from "../../src/factory/MultiOwnerMSCAFactory.sol"; import {IEntryPoint} from "../../src/interfaces/erc4337/IEntryPoint.sol"; -import {FunctionReference} from "../../src/interfaces/IPluginManager.sol"; import {MultiOwnerPlugin} from "../../src/plugins/owner/MultiOwnerPlugin.sol"; -import {TokenReceiverPlugin} from "../../src/plugins/TokenReceiverPlugin.sol"; import {MockERC1155} from "../mocks/tokens/MockERC1155.sol"; import {MockERC777} from "../mocks/tokens/MockERC777.sol"; -contract TokenReceiverPluginTest is Test, IERC1155Receiver, AccountStorageV1 { +contract TokenReceiverTest is Test, IERC1155Receiver { UpgradeableModularAccount public acct; - TokenReceiverPlugin public plugin; ERC721PresetMinterPauserAutoId public t0; MockERC777 public t1; @@ -74,7 +70,6 @@ contract TokenReceiverPluginTest is Test, IERC1155Receiver, AccountStorageV1 { owners = new address[](1); owners[0] = owner; acct = UpgradeableModularAccount(payable(factory.createAccount(0, owners))); - plugin = new TokenReceiverPlugin(); t0 = new ERC721PresetMinterPauserAutoId("t0", "t0", ""); t0.mint(address(this)); @@ -92,44 +87,13 @@ contract TokenReceiverPluginTest is Test, IERC1155Receiver, AccountStorageV1 { } } - function _initPlugin() internal { - vm.startPrank(owner); - acct.installPlugin( - address(plugin), keccak256(abi.encode(plugin.pluginManifest())), bytes(""), new FunctionReference[](0) - ); - vm.stopPrank(); - } - - function test_failERC721Transfer() public { - vm.expectRevert( - abi.encodeWithSelector( - UpgradeableModularAccount.RuntimeValidationFunctionMissing.selector, - IERC721Receiver.onERC721Received.selector - ) - ); - t0.safeTransferFrom(address(this), address(acct), _TOKEN_ID); - } - function test_passERC721Transfer() public { - _initPlugin(); assertEq(t0.ownerOf(_TOKEN_ID), address(this)); t0.safeTransferFrom(address(this), address(acct), _TOKEN_ID); assertEq(t0.ownerOf(_TOKEN_ID), address(acct)); } - function test_failERC777Transfer() public { - vm.expectRevert( - abi.encodeWithSelector( - UpgradeableModularAccount.RuntimeValidationFunctionMissing.selector, - IERC777Recipient.tokensReceived.selector - ) - ); - t1.transfer(address(acct), _TOKEN_AMOUNT); - } - function test_passERC777Transfer() public { - _initPlugin(); - assertEq(t1.balanceOf(address(this)), _TOKEN_AMOUNT); assertEq(t1.balanceOf(address(acct)), 0); t1.transfer(address(acct), _TOKEN_AMOUNT); @@ -137,19 +101,7 @@ contract TokenReceiverPluginTest is Test, IERC1155Receiver, AccountStorageV1 { assertEq(t1.balanceOf(address(acct)), _TOKEN_AMOUNT); } - function test_failERC1155Transfer() public { - // for 1155, reverts are caught in a try catch and bubbled up with a diff reason - vm.expectRevert("ERC1155: transfer to non-ERC1155Receiver implementer"); - t2.safeTransferFrom(address(this), address(acct), _TOKEN_ID, _TOKEN_AMOUNT, ""); - - // for 1155, reverts are caught in a try catch and bubbled up with a diff reason - vm.expectRevert("ERC1155: transfer to non-ERC1155Receiver implementer"); - t2.safeBatchTransferFrom(address(this), address(acct), tokenIds, tokenAmts, ""); - } - function test_passERC1155Transfer() public { - _initPlugin(); - assertEq(t2.balanceOf(address(this), _TOKEN_ID), _TOKEN_AMOUNT); assertEq(t2.balanceOf(address(acct), _TOKEN_ID), 0); t2.safeTransferFrom(address(this), address(acct), _TOKEN_ID, _TOKEN_AMOUNT, ""); @@ -167,20 +119,7 @@ contract TokenReceiverPluginTest is Test, IERC1155Receiver, AccountStorageV1 { } } - function test_failIntrospection() public { - bool isSupported; - - isSupported = acct.supportsInterface(type(IERC721Receiver).interfaceId); - assertEq(isSupported, false); - isSupported = acct.supportsInterface(type(IERC777Recipient).interfaceId); - assertEq(isSupported, false); - isSupported = acct.supportsInterface(type(IERC1155Receiver).interfaceId); - assertEq(isSupported, false); - } - function test_passIntrospection() public { - _initPlugin(); - bool isSupported; isSupported = acct.supportsInterface(type(IERC721Receiver).interfaceId); diff --git a/test/account/UpgradeableModularAccount.t.sol b/test/account/UpgradeableModularAccount.t.sol index 32d21266..20acf6bb 100644 --- a/test/account/UpgradeableModularAccount.t.sol +++ b/test/account/UpgradeableModularAccount.t.sol @@ -36,7 +36,6 @@ import {Call} from "../../src/interfaces/IStandardExecutor.sol"; import {IMultiOwnerPlugin} from "../../src/plugins/owner/IMultiOwnerPlugin.sol"; import {MultiOwnerPlugin} from "../../src/plugins/owner/MultiOwnerPlugin.sol"; import {SessionKeyPlugin} from "../../src/plugins/session/SessionKeyPlugin.sol"; -import {TokenReceiverPlugin} from "../../src/plugins/TokenReceiverPlugin.sol"; import {Counter} from "../mocks/Counter.sol"; import {MockPlugin} from "../mocks/MockPlugin.sol"; @@ -46,7 +45,6 @@ contract UpgradeableModularAccountTest is Test { IEntryPoint public entryPoint; address payable public beneficiary; MultiOwnerPlugin public multiOwnerPlugin; - TokenReceiverPlugin public tokenReceiverPlugin; SessionKeyPlugin public sessionKeyPlugin; MultiOwnerMSCAFactory public factory; address public accountImplementation; @@ -76,7 +74,6 @@ contract UpgradeableModularAccountTest is Test { vm.deal(beneficiary, 1 wei); multiOwnerPlugin = new MultiOwnerPlugin(); - tokenReceiverPlugin = new TokenReceiverPlugin(); sessionKeyPlugin = new SessionKeyPlugin(); accountImplementation = address(new UpgradeableModularAccount(entryPoint)); bytes32 manifestHash = keccak256(abi.encode(multiOwnerPlugin.pluginManifest())); diff --git a/test/account/UpgradeableModularAccountPluginManager.t.sol b/test/account/UpgradeableModularAccountPluginManager.t.sol index 78e01bfc..a90869fe 100644 --- a/test/account/UpgradeableModularAccountPluginManager.t.sol +++ b/test/account/UpgradeableModularAccountPluginManager.t.sol @@ -37,7 +37,6 @@ import {Call} from "../../src/interfaces/IStandardExecutor.sol"; import {IMultiOwnerPlugin} from "../../src/plugins/owner/IMultiOwnerPlugin.sol"; import {MultiOwnerPlugin} from "../../src/plugins/owner/MultiOwnerPlugin.sol"; import {SessionKeyPlugin} from "../../src/plugins/session/SessionKeyPlugin.sol"; -import {TokenReceiverPlugin} from "../../src/plugins/TokenReceiverPlugin.sol"; import {Counter} from "../mocks/Counter.sol"; import {MockPlugin} from "../mocks/MockPlugin.sol"; import { @@ -52,7 +51,6 @@ contract UpgradeableModularAccountPluginManagerTest is Test { IEntryPoint public entryPoint; address payable public beneficiary; MultiOwnerPlugin public multiOwnerPlugin; - TokenReceiverPlugin public tokenReceiverPlugin; SessionKeyPlugin public sessionKeyPlugin; MultiOwnerMSCAFactory public factory; address public implementation; @@ -86,7 +84,6 @@ contract UpgradeableModularAccountPluginManagerTest is Test { vm.deal(beneficiary, 1 wei); multiOwnerPlugin = new MultiOwnerPlugin(); - tokenReceiverPlugin = new TokenReceiverPlugin(); sessionKeyPlugin = new SessionKeyPlugin(); implementation = address(new UpgradeableModularAccount(entryPoint)); bytes32 manifestHash = keccak256(abi.encode(multiOwnerPlugin.pluginManifest())); @@ -142,21 +139,10 @@ contract UpgradeableModularAccountPluginManagerTest is Test { dependencies: dependencies }); - manifestHash = keccak256(abi.encode(tokenReceiverPlugin.pluginManifest())); - vm.expectEmit(true, true, true, true); - emit PluginInstalled(address(tokenReceiverPlugin), manifestHash, new FunctionReference[](0)); - IPluginManager(account2).installPlugin({ - plugin: address(tokenReceiverPlugin), - manifestHash: manifestHash, - pluginInstallData: abi.encode(uint48(1 days)), - dependencies: new FunctionReference[](0) - }); - address[] memory plugins = IAccountLoupe(account2).getInstalledPlugins(); - assertEq(plugins.length, 3); - assertEq(plugins[0], address(tokenReceiverPlugin)); - assertEq(plugins[1], address(sessionKeyPlugin)); - assertEq(plugins[2], address(multiOwnerPlugin)); + assertEq(plugins.length, 2); + assertEq(plugins[0], address(sessionKeyPlugin)); + assertEq(plugins[1], address(multiOwnerPlugin)); } function test_installPlugin_ExecuteFromPlugin_PermittedExecSelectorNotInstalled() public { @@ -184,9 +170,9 @@ contract UpgradeableModularAccountPluginManagerTest is Test { vm.expectRevert(abi.encodeWithSelector(PluginManagerInternals.InvalidPluginManifest.selector)); IPluginManager(account2).installPlugin({ - plugin: address(tokenReceiverPlugin), + plugin: address(sessionKeyPlugin), manifestHash: bytes32(0), - pluginInstallData: abi.encode(uint48(1 days)), + pluginInstallData: abi.encode(owners1), dependencies: new FunctionReference[](0) }); } @@ -209,23 +195,17 @@ contract UpgradeableModularAccountPluginManagerTest is Test { function test_installPlugin_alreadyInstalled() public { vm.startPrank(owner2); - bytes32 manifestHash = keccak256(abi.encode(tokenReceiverPlugin.pluginManifest())); - IPluginManager(account2).installPlugin({ - plugin: address(tokenReceiverPlugin), - manifestHash: manifestHash, - pluginInstallData: abi.encode(uint48(1 days)), - dependencies: new FunctionReference[](0) - }); + bytes32 manifestHash = keccak256(abi.encode(multiOwnerPlugin.pluginManifest())); vm.expectRevert( abi.encodeWithSelector( - PluginManagerInternals.PluginAlreadyInstalled.selector, address(tokenReceiverPlugin) + PluginManagerInternals.PluginAlreadyInstalled.selector, address(multiOwnerPlugin) ) ); IPluginManager(account2).installPlugin({ - plugin: address(tokenReceiverPlugin), + plugin: address(multiOwnerPlugin), manifestHash: manifestHash, - pluginInstallData: abi.encode(uint48(1 days)), + pluginInstallData: abi.encode(owners1), dependencies: new FunctionReference[](0) }); } @@ -283,7 +263,7 @@ contract UpgradeableModularAccountPluginManagerTest is Test { MockPlugin mockPluginBad = new MockPlugin(manifestBad); bytes32 manifestHashBad = keccak256(abi.encode(mockPluginBad.pluginManifest())); - vm.expectRevert(PluginManagerInternals.IPluginInterfaceNotAllowed.selector); + vm.expectRevert(PluginManagerInternals.InterfaceNotAllowed.selector); IPluginManager(account2).installPlugin({ plugin: address(mockPluginBad), manifestHash: manifestHashBad, diff --git a/test/factory/MultiOwnerTokenReceiverFactoryTest.t.sol b/test/factory/MultiOwnerTokenReceiverFactoryTest.t.sol deleted file mode 100644 index 70b55a60..00000000 --- a/test/factory/MultiOwnerTokenReceiverFactoryTest.t.sol +++ /dev/null @@ -1,257 +0,0 @@ -// This file is part of Modular Account. -// -// Copyright 2024 Alchemy Insights, Inc. -// -// SPDX-License-Identifier: GPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General -// Public License as published by the Free Software Foundation, either version 3 of the License, or (at your -// option) any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the -// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along with this program. If not, see -// . - -pragma solidity ^0.8.22; - -import {Test} from "forge-std/Test.sol"; - -import {ERC721PresetMinterPauserAutoId} from - "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; -import {EntryPoint} from "@eth-infinitism/account-abstraction/core/EntryPoint.sol"; -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; - -import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; -import {MultiOwnerTokenReceiverMSCAFactory} from "../../src/factory/MultiOwnerTokenReceiverMSCAFactory.sol"; -import {IEntryPoint} from "../../src/interfaces/erc4337/IEntryPoint.sol"; -import {MultiOwnerPlugin} from "../../src/plugins/owner/MultiOwnerPlugin.sol"; -import {TokenReceiverPlugin} from "../../src/plugins/TokenReceiverPlugin.sol"; -import {MockERC1155} from "../mocks/tokens/MockERC1155.sol"; -import {MockERC777} from "../mocks/tokens/MockERC777.sol"; - -contract MultiOwnerTokenReceiverMSCAFactoryTest is Test { - using ECDSA for bytes32; - - EntryPoint public entryPoint; - MultiOwnerTokenReceiverMSCAFactory public factory; - MultiOwnerPlugin public multiOwnerPlugin; - TokenReceiverPlugin public tokenReceiverPlugin; - address public impl; - ERC721PresetMinterPauserAutoId public t0; - MockERC777 public t1; - MockERC1155 public t2; - - address public owner1 = address(1); - address public owner2 = address(2); - address public nftHolder = address(3); - - address[] public owners; - address[] public largeOwners; - uint256[] public tokenIds; - uint256[] public tokenAmts; - uint256[] public zeroTokenAmts; - - uint256 internal constant _TOKEN_AMOUNT = 1 ether; - uint256 internal constant _TOKEN_ID = 0; - uint256 internal constant _BATCH_TOKEN_IDS = 5; - uint256 internal constant _MAX_OWNERS_ON_CREATION = 100; - - bytes32 internal constant _IMPLEMENTATION_SLOT = - 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - - function setUp() public { - owners.push(owner1); - owners.push(owner2); - entryPoint = new EntryPoint(); - impl = address(new UpgradeableModularAccount(IEntryPoint(address(entryPoint)))); - multiOwnerPlugin = new MultiOwnerPlugin(); - tokenReceiverPlugin = new TokenReceiverPlugin(); - bytes32 ownerManifestHash = keccak256(abi.encode(multiOwnerPlugin.pluginManifest())); - bytes32 tokenReceiverManifestHash = keccak256(abi.encode(tokenReceiverPlugin.pluginManifest())); - factory = new MultiOwnerTokenReceiverMSCAFactory( - address(this), - address(multiOwnerPlugin), - address(tokenReceiverPlugin), - impl, - ownerManifestHash, - tokenReceiverManifestHash, - IEntryPoint(address(entryPoint)) - ); - vm.deal(nftHolder, 100 ether); - - t0 = new ERC721PresetMinterPauserAutoId("t0", "t0", ""); - t0.mint(nftHolder); - - t1 = new MockERC777(); - t1.mint(nftHolder, _TOKEN_AMOUNT); - - t2 = new MockERC1155(); - t2.mint(nftHolder, _TOKEN_ID, _TOKEN_AMOUNT); - for (uint256 i = 1; i < _BATCH_TOKEN_IDS; i++) { - t2.mint(nftHolder, i, _TOKEN_AMOUNT); - tokenIds.push(i); - tokenAmts.push(_TOKEN_AMOUNT); - zeroTokenAmts.push(0); - } - - for (uint160 i = 0; i < _MAX_OWNERS_ON_CREATION; i++) { - largeOwners.push(address(i + 1)); - } - } - - function test_addressMatch() public { - address predicted = factory.getAddress(0, owners); - address deployed = factory.createAccount(0, owners); - assertEq(predicted, deployed); - } - - function test_deploy() public { - address deployed = factory.createAccount(0, owners); - - // test that the deployed account is initialized - assertEq(address(UpgradeableModularAccount(payable(deployed)).entryPoint()), address(entryPoint)); - - // test that the deployed account installed owner plugin correctly - address[] memory actualOwners = multiOwnerPlugin.ownersOf(deployed); - assertEq(actualOwners.length, 2); - assertEq(actualOwners[0], owner2); - assertEq(actualOwners[1], owner1); - } - - function test_receiveTokens() public { - address acct = factory.createAccount(0, owners); - - vm.startPrank(nftHolder); - - // test that it can receive tokens - assertEq(t0.ownerOf(_TOKEN_ID), nftHolder); - t0.safeTransferFrom(nftHolder, acct, _TOKEN_ID); - assertEq(t0.ownerOf(_TOKEN_ID), acct); - - assertEq(t1.balanceOf(nftHolder), _TOKEN_AMOUNT); - assertEq(t1.balanceOf(acct), 0); - t1.transfer(acct, _TOKEN_AMOUNT); - assertEq(t1.balanceOf(nftHolder), 0); - assertEq(t1.balanceOf(acct), _TOKEN_AMOUNT); - - assertEq(t2.balanceOf(nftHolder, _TOKEN_ID), _TOKEN_AMOUNT); - assertEq(t2.balanceOf(acct, _TOKEN_ID), 0); - t2.safeTransferFrom(nftHolder, acct, _TOKEN_ID, _TOKEN_AMOUNT, ""); - assertEq(t2.balanceOf(nftHolder, _TOKEN_ID), 0); - assertEq(t2.balanceOf(acct, _TOKEN_ID), _TOKEN_AMOUNT); - - for (uint256 i = 1; i < _BATCH_TOKEN_IDS; i++) { - assertEq(t2.balanceOf(nftHolder, i), _TOKEN_AMOUNT); - assertEq(t2.balanceOf(acct, i), 0); - } - t2.safeBatchTransferFrom(nftHolder, acct, tokenIds, tokenAmts, ""); - for (uint256 i = 1; i < _BATCH_TOKEN_IDS; i++) { - assertEq(t2.balanceOf(nftHolder, i), 0); - assertEq(t2.balanceOf(acct, i), _TOKEN_AMOUNT); - } - } - - function test_deployCollision() public { - address deployed = factory.createAccount(0, owners); - - uint256 gasStart = gasleft(); - - // deploy 2nd time which should short circuit - // test for short circuit -> call should cost less than a CREATE2, or 32000 gas - address secondDeploy = factory.createAccount(0, owners); - - assertApproxEqAbs(gasleft(), gasStart, 31999); - assertEq(deployed, secondDeploy); - } - - function test_deployedAccountHasCorrectPlugins() public { - address deployed = factory.createAccount(0, owners); - - // check installed plugins on account - address[] memory plugins = UpgradeableModularAccount(payable(deployed)).getInstalledPlugins(); - assertEq(plugins.length, 2); - assertEq(plugins[0], address(tokenReceiverPlugin)); - assertEq(plugins[1], address(multiOwnerPlugin)); - } - - function test_addStake() public { - assertEq(entryPoint.balanceOf(address(factory)), 0); - vm.deal(address(this), 100 ether); - factory.addStake{value: 10 ether}(10 hours, 10 ether); - assertEq(entryPoint.getDepositInfo(address(factory)).stake, 10 ether); - } - - function test_unlockStake() public { - test_addStake(); - factory.unlockStake(); - assertEq(entryPoint.getDepositInfo(address(factory)).withdrawTime, block.timestamp + 10 hours); - } - - function test_withdrawStake() public { - test_unlockStake(); - vm.warp(10 hours); - vm.expectRevert("Stake withdrawal is not due"); - factory.withdrawStake(payable(address(this))); - assertEq(address(this).balance, 90 ether); - vm.warp(10 hours + 1); - factory.withdrawStake(payable(address(this))); - assertEq(address(this).balance, 100 ether); - } - - function test_withdraw() public { - factory.addStake{value: 10 ether}(10 hours, 1 ether); - assertEq(address(factory).balance, 9 ether); - factory.withdraw(payable(address(this)), address(0), 0); // amount = balance if native currency - assertEq(address(factory).balance, 0); - } - - function test_2StepOwnershipTransfer() public { - assertEq(factory.owner(), address(this)); - factory.transferOwnership(owner1); - assertEq(factory.owner(), address(this)); - vm.prank(owner1); - factory.acceptOwnership(); - assertEq(factory.owner(), owner1); - } - - function test_getAddressWithMaxOwnersAndDeploy() public { - address addr = factory.getAddress(0, largeOwners); - assertEq(addr, factory.createAccount(0, largeOwners)); - } - - function test_getAddressWithTooManyOwners() public { - largeOwners.push(address(101)); - vm.expectRevert(MultiOwnerTokenReceiverMSCAFactory.OwnersLimitExceeded.selector); - factory.getAddress(0, largeOwners); - } - - function test_getAddressWithUnsortedOwners() public { - address[] memory tempOwners = new address[](2); - tempOwners[0] = address(2); - tempOwners[1] = address(1); - vm.expectRevert(MultiOwnerTokenReceiverMSCAFactory.InvalidOwner.selector); - factory.getAddress(0, tempOwners); - } - - function test_deployWithDuplicateOwners() public { - address[] memory tempOwners = new address[](2); - tempOwners[0] = address(1); - tempOwners[1] = address(1); - vm.expectRevert(MultiOwnerTokenReceiverMSCAFactory.InvalidOwner.selector); - factory.createAccount(0, tempOwners); - } - - function test_deployWithUnsortedOwners() public { - address[] memory tempOwners = new address[](2); - tempOwners[0] = address(2); - tempOwners[1] = address(1); - vm.expectRevert(MultiOwnerTokenReceiverMSCAFactory.InvalidOwner.selector); - factory.createAccount(0, tempOwners); - } - - // to receive funds from withdraw - receive() external payable {} -} diff --git a/test/helpers/KnownSelectors.t.sol b/test/helpers/KnownSelectors.t.sol index e7169d78..78e513f4 100644 --- a/test/helpers/KnownSelectors.t.sol +++ b/test/helpers/KnownSelectors.t.sol @@ -22,7 +22,10 @@ import {Test} from "forge-std/Test.sol"; import {BaseAccount} from "@eth-infinitism/account-abstraction/core/BaseAccount.sol"; import {IAggregator} from "@eth-infinitism/account-abstraction/interfaces/IAggregator.sol"; import {IPaymaster} from "@eth-infinitism/account-abstraction/interfaces/IPaymaster.sol"; +import {IERC1155Receiver} from "@openzeppelin/contracts/interfaces/IERC1155Receiver.sol"; +import {IERC777Recipient} from "@openzeppelin/contracts/interfaces/IERC777Recipient.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {KnownSelectors} from "../../src/helpers/KnownSelectors.sol"; @@ -68,6 +71,17 @@ contract KnownSelectorsTest is Test { assertTrue(KnownSelectors.isNativeFunction(IAccountLoupe.getPreValidationHooks.selector)); assertTrue(KnownSelectors.isNativeFunction(IAccountLoupe.getInstalledPlugins.selector)); + // TokenReceiver methods + assertTrue(KnownSelectors.isNativeFunction(IAccountLoupe.getExecutionFunctionConfig.selector)); + assertTrue(KnownSelectors.isNativeFunction(IAccountLoupe.getExecutionHooks.selector)); + assertTrue(KnownSelectors.isNativeFunction(IAccountLoupe.getPreValidationHooks.selector)); + assertTrue(KnownSelectors.isNativeFunction(IAccountLoupe.getInstalledPlugins.selector)); + + assertTrue(KnownSelectors.isNativeFunction(IERC777Recipient.tokensReceived.selector)); + assertTrue(KnownSelectors.isNativeFunction(IERC721Receiver.onERC721Received.selector)); + assertTrue(KnownSelectors.isNativeFunction(IERC1155Receiver.onERC1155Received.selector)); + assertTrue(KnownSelectors.isNativeFunction(IERC1155Receiver.onERC1155BatchReceived.selector)); + assertFalse(KnownSelectors.isNativeFunction(IPaymaster.validatePaymasterUserOp.selector)); } diff --git a/test/upgrade/MSCAToMSCA.t.sol b/test/upgrade/MSCAToMSCA.t.sol index 59b634fc..6405dc19 100644 --- a/test/upgrade/MSCAToMSCA.t.sol +++ b/test/upgrade/MSCAToMSCA.t.sol @@ -22,11 +22,10 @@ import {Test} from "forge-std/Test.sol"; import {EntryPoint} from "@eth-infinitism/account-abstraction/core/EntryPoint.sol"; import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; -import {MultiOwnerTokenReceiverMSCAFactory} from "../../src/factory/MultiOwnerTokenReceiverMSCAFactory.sol"; +import {MultiOwnerMSCAFactory} from "../../src/factory/MultiOwnerMSCAFactory.sol"; import {IEntryPoint} from "../../src/interfaces/erc4337/IEntryPoint.sol"; import {Call} from "../../src/interfaces/IStandardExecutor.sol"; import {MultiOwnerPlugin} from "../../src/plugins/owner/MultiOwnerPlugin.sol"; -import {TokenReceiverPlugin} from "../../src/plugins/TokenReceiverPlugin.sol"; import {MockERC20} from "../mocks/tokens/MockERC20.sol"; import {Utils} from "../Utils.sol"; @@ -39,7 +38,6 @@ contract MSCAToMSCATest is Test { UpgradeableModularAccount public msca; MultiOwnerPlugin public multiOwnerPlugin; - TokenReceiverPlugin public tokenReceiverPlugin; address public mscaImpl1; address public mscaImpl2; @@ -52,17 +50,9 @@ contract MSCAToMSCATest is Test { mscaImpl1 = address(new UpgradeableModularAccount(entryPoint)); mscaImpl2 = address(new UpgradeableModularAccount(entryPoint)); multiOwnerPlugin = new MultiOwnerPlugin(); - tokenReceiverPlugin = new TokenReceiverPlugin(); bytes32 ownerManifestHash = keccak256(abi.encode(multiOwnerPlugin.pluginManifest())); - bytes32 tokenReceiverManifestHash = keccak256(abi.encode(tokenReceiverPlugin.pluginManifest())); - MultiOwnerTokenReceiverMSCAFactory factory = new MultiOwnerTokenReceiverMSCAFactory( - address(this), - address(multiOwnerPlugin), - address(tokenReceiverPlugin), - mscaImpl1, - ownerManifestHash, - tokenReceiverManifestHash, - entryPoint + MultiOwnerMSCAFactory factory = new MultiOwnerMSCAFactory( + address(this), address(multiOwnerPlugin), mscaImpl1, ownerManifestHash, entryPoint ); msca = UpgradeableModularAccount(payable(factory.createAccount(0, owners))); vm.deal(address(msca), 2 ether);