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