Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: move token receiver functions into account #124

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably not the PR for it, but we do support contract owners too.

- [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

Expand Down Expand Up @@ -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
```
Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ out = 'out'
test = 'test'
libs = ['lib']
optimizer = true
optimizer_runs = 1600
optimizer_runs = 900
ignored_error_codes = []

[fuzz]
Expand Down
56 changes: 9 additions & 47 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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)) {
Expand Down
11 changes: 7 additions & 4 deletions src/account/PluginManagerInternals.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The following interface types are also not relevant, although it doesn't really hurt to add them

  • type(IERC721Receiver).interfaceId
  • type(IERC777Recipient).interfaceId
  • type(IERC1155Receiver).interfaceId

Copy link
Collaborator Author

@jaypaik jaypaik Jan 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. I'll probably leave this one as is since it doesn't hurt, and adding this in will increase contract size.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to reduce the contract size || interfaceId == _INVALID_INTERFACE_ID
is also not really neccesary as it also checked in supportsInterface

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed! But I think this is ever so slightly different, which may justify this one.

  • Without this check, a plugin wanting to add _INVALID_INTERFACE_ID will successfully install but always fail when supportsInterface(_INVALID_INTERFACE_ID) is called, which could be unexpected.
  • Without the token receiver interface checks here, a plugin wanting to add type(IERC721Receiver).interfaceId will successfully install, and supportsInterface(type(IERC721Receiver).interfaceId) will return true as expected.

}
storage_.supportedInterfaces[interfaceId] += 1;
}
Expand Down
80 changes: 68 additions & 12 deletions src/account/UpgradeableModularAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -51,6 +54,9 @@ contract UpgradeableModularAccount is
IAccountInitializable,
IAccountView,
IERC165,
IERC721Receiver,
IERC777Recipient,
IERC1155Receiver,
IPluginExecutor,
IStandardExecutor,
UUPSUpgradeable
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
jaypaik marked this conversation as resolved.
Show resolved Hide resolved
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);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to override onERC721Received(), while still keeping the hooks, it might be useful
that onERC721Received() calls another function (which is initially empty), that can be overriden

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. Addressed in #127 !


/// @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
Expand All @@ -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;
Expand Down Expand Up @@ -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
{
Expand Down
Loading
Loading