Skip to content

Commit

Permalink
Merge branch 'v0.8-develop' into zer0dot/semi-modular-experiments
Browse files Browse the repository at this point in the history
  • Loading branch information
Zer0dot committed Jul 25, 2024
2 parents b5c593b + b71d6c9 commit d8b0207
Show file tree
Hide file tree
Showing 67 changed files with 2,085 additions and 1,915 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Reference implementation for [ERC-6900](https://eips.ethereum.org/EIPS/eip-6900). It is an early draft implementation.

The implementation includes an upgradable modular account with two plugins (`SingleSignerValidation` and `TokenReceiverPlugin`). It is compliant with ERC-6900 with the latest updates.
The implementation includes an upgradable modular account with three modules (`SingleSignerValidation`, `TokenReceiverModule`, and `AllowlistModule`). It is compliant with ERC-6900 with the latest updates.

## Important Callouts

Expand All @@ -11,7 +11,7 @@ The implementation includes an upgradable modular account with two plugins (`Sin

## Development

Anyone is welcome to submit feedback and/or PRs to improve code or add Plugins.
Anyone is welcome to submit feedback and/or PRs to improve code.

### Testing

Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ runs = 5000
depth = 32

[profile.deep.fuzz]
runs = 10000
runs = 100000

[profile.deep.invariant]
runs = 5000
Expand Down
83 changes: 83 additions & 0 deletions src/account/AccountFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntryPoint.sol";

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";

import {UpgradeableModularAccount} from "../account/UpgradeableModularAccount.sol";
import {ValidationConfigLib} from "../helpers/ValidationConfigLib.sol";

contract AccountFactory is Ownable {
UpgradeableModularAccount public immutable ACCOUNT_IMPL;
bytes32 private immutable _PROXY_BYTECODE_HASH;
uint32 public constant UNSTAKE_DELAY = 1 weeks;
IEntryPoint public immutable ENTRY_POINT;
address public immutable SINGLE_SIGNER_VALIDATION;

constructor(IEntryPoint _entryPoint, UpgradeableModularAccount _accountImpl, address _singleSignerValidation)
Ownable(msg.sender)
{
ENTRY_POINT = _entryPoint;
_PROXY_BYTECODE_HASH =
keccak256(abi.encodePacked(type(ERC1967Proxy).creationCode, abi.encode(address(_accountImpl), "")));
ACCOUNT_IMPL = _accountImpl;
SINGLE_SIGNER_VALIDATION = _singleSignerValidation;
}

/**
* Create an account, and return its address.
* Returns the address even if the account is already deployed.
* Note that during user operation execution, this method is called only if the account is not deployed.
* This method returns an existing account address so that entryPoint.getSenderAddress() would work even after
* account creation
*/
function createAccount(address owner, uint256 salt, uint32 entityId)
external
returns (UpgradeableModularAccount)
{
bytes32 combinedSalt = getSalt(owner, salt, entityId);
address addr = Create2.computeAddress(combinedSalt, _PROXY_BYTECODE_HASH);

// short circuit if exists
if (addr.code.length == 0) {
bytes memory pluginInstallData = abi.encode(entityId, owner);
// not necessary to check return addr since next call will fail if so
new ERC1967Proxy{salt: combinedSalt}(address(ACCOUNT_IMPL), "");
// point proxy to actual implementation and init plugins
UpgradeableModularAccount(payable(addr)).initializeWithValidation(
ValidationConfigLib.pack(SINGLE_SIGNER_VALIDATION, entityId, true, true),
new bytes4[](0),
pluginInstallData,
new bytes[](0)
);
}

return UpgradeableModularAccount(payable(addr));
}

function addStake() external payable onlyOwner {
ENTRY_POINT.addStake{value: msg.value}(UNSTAKE_DELAY);
}

function unlockStake() external onlyOwner {
ENTRY_POINT.unlockStake();
}

function withdrawStake(address payable withdrawAddress) external onlyOwner {
ENTRY_POINT.withdrawStake(withdrawAddress);
}

/**
* Calculate the counterfactual address of this account as it would be returned by createAccount()
*/
function getAddress(address owner, uint256 salt, uint32 entityId) external view returns (address) {
return Create2.computeAddress(getSalt(owner, salt, entityId), _PROXY_BYTECODE_HASH);
}

function getSalt(address owner, uint256 salt, uint32 entityId) public pure returns (bytes32) {
return keccak256(abi.encodePacked(owner, salt, entityId));
}
}
43 changes: 24 additions & 19 deletions src/account/AccountLoupe.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,33 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeab
import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

import {HookConfigLib} from "../helpers/HookConfigLib.sol";
import {ExecutionHook, IAccountLoupe} from "../interfaces/IAccountLoupe.sol";
import {IPluginManager, PluginEntity} from "../interfaces/IPluginManager.sol";
import {HookConfig, IModuleManager, ModuleEntity} from "../interfaces/IModuleManager.sol";
import {IStandardExecutor} from "../interfaces/IStandardExecutor.sol";
import {getAccountStorage, toExecutionHook, toSelector} from "./AccountStorage.sol";
import {getAccountStorage, toHookConfig, toSelector} from "./AccountStorage.sol";

abstract contract AccountLoupe is IAccountLoupe {
using EnumerableSet for EnumerableSet.Bytes32Set;
using EnumerableMap for EnumerableMap.AddressToUintMap;
using HookConfigLib for HookConfig;

/// @inheritdoc IAccountLoupe
function getExecutionFunctionHandler(bytes4 selector) external view override returns (address plugin) {
function getExecutionFunctionHandler(bytes4 selector) external view override returns (address module) {
if (
selector == IStandardExecutor.execute.selector || selector == IStandardExecutor.executeBatch.selector
|| selector == UUPSUpgradeable.upgradeToAndCall.selector
|| selector == IPluginManager.installPlugin.selector
|| selector == IPluginManager.uninstallPlugin.selector
|| selector == IModuleManager.installModule.selector
|| selector == IModuleManager.uninstallModule.selector
) {
return address(this);
}

return getAccountStorage().selectorData[selector].plugin;
return getAccountStorage().selectorData[selector].module;
}

/// @inheritdoc IAccountLoupe
function getSelectors(PluginEntity validationFunction) external view returns (bytes4[] memory) {
function getSelectors(ModuleEntity validationFunction) external view returns (bytes4[] memory) {
uint256 length = getAccountStorage().validationData[validationFunction].selectors.length();

bytes4[] memory selectors = new bytes4[](length);
Expand All @@ -56,13 +58,17 @@ abstract contract AccountLoupe is IAccountLoupe {

for (uint256 i = 0; i < executionHooksLength; ++i) {
bytes32 key = hooks.at(i);
ExecutionHook memory execHook = execHooks[i];
(execHook.hookFunction, execHook.isPreHook, execHook.isPostHook) = toExecutionHook(key);
HookConfig hookConfig = toHookConfig(key);
execHooks[i] = ExecutionHook({
hookFunction: hookConfig.moduleEntity(),
isPreHook: hookConfig.hasPreHook(),
isPostHook: hookConfig.hasPostHook()
});
}
}

/// @inheritdoc IAccountLoupe
function getPermissionHooks(PluginEntity validationFunction)
function getPermissionHooks(ModuleEntity validationFunction)
external
view
override
Expand All @@ -74,23 +80,22 @@ abstract contract AccountLoupe is IAccountLoupe {
permissionHooks = new ExecutionHook[](executionHooksLength);
for (uint256 i = 0; i < executionHooksLength; ++i) {
bytes32 key = hooks.at(i);
ExecutionHook memory execHook = permissionHooks[i];
(execHook.hookFunction, execHook.isPreHook, execHook.isPostHook) = toExecutionHook(key);
HookConfig hookConfig = toHookConfig(key);
permissionHooks[i] = ExecutionHook({
hookFunction: hookConfig.moduleEntity(),
isPreHook: hookConfig.hasPreHook(),
isPostHook: hookConfig.hasPostHook()
});
}
}

/// @inheritdoc IAccountLoupe
function getPreValidationHooks(PluginEntity validationFunction)
function getPreValidationHooks(ModuleEntity validationFunction)
external
view
override
returns (PluginEntity[] memory preValidationHooks)
returns (ModuleEntity[] memory preValidationHooks)
{
preValidationHooks = getAccountStorage().validationData[validationFunction].preValidationHooks;
}

/// @inheritdoc IAccountLoupe
function getInstalledPlugins() external view override returns (address[] memory pluginAddresses) {
pluginAddresses = getAccountStorage().pluginManifestHashes.keys();
}
}
45 changes: 17 additions & 28 deletions src/account/AccountStorage.sol
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.25;

import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

import {ExecutionHook} from "../interfaces/IAccountLoupe.sol";
import {PluginEntity} from "../interfaces/IPluginManager.sol";
import {HookConfig, ModuleEntity} from "../interfaces/IModuleManager.sol";

// bytes = keccak256("ERC6900.UpgradeableModularAccount.Storage")
bytes32 constant _ACCOUNT_STORAGE_SLOT = 0x9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e40;

// Represents data associated with a specifc function selector.
struct SelectorData {
// The plugin that implements this execution function.
// The module that implements this execution function.
// If this is a native function, the address must remain address(0).
address plugin;
address module;
// Whether or not the function needs runtime validation, or can be called by anyone. The function can still be
// state changing if this flag is set to true.
// Note that even if this is set to true, user op validation will still be required, otherwise anyone could
Expand All @@ -34,7 +32,7 @@ struct ValidationData {
// Whether, in the case this is an appended bytecode validation, the validation is disabled
bool isAppendedBytecodeValidationDisabled;
// The pre validation hooks for this validation function.
PluginEntity[] preValidationHooks;
ModuleEntity[] preValidationHooks;
// Permission hooks for this validation function.
EnumerableSet.Bytes32Set permissionHooks;
// The set of selectors that may be validated by this validation function.
Expand All @@ -45,11 +43,9 @@ struct AccountStorage {
// AccountStorageInitializable variables
uint8 initialized;
bool initializing;
// Plugin metadata storage
EnumerableMap.AddressToUintMap pluginManifestHashes;
// Execution functions and their associated functions
mapping(bytes4 => SelectorData) selectorData;
mapping(PluginEntity validationFunction => ValidationData) validationData;
mapping(ModuleEntity validationFunction => ValidationData) validationData;
// For ERC165 introspection
mapping(bytes4 => uint256) supportedIfaces;
}
Expand All @@ -62,32 +58,25 @@ function getAccountStorage() pure returns (AccountStorage storage _storage) {

using EnumerableSet for EnumerableSet.Bytes32Set;

function toSetValue(PluginEntity pluginEntity) pure returns (bytes32) {
return bytes32(PluginEntity.unwrap(pluginEntity));
function toSetValue(ModuleEntity moduleEntity) pure returns (bytes32) {
return bytes32(ModuleEntity.unwrap(moduleEntity));
}

function toPluginEntity(bytes32 setValue) pure returns (PluginEntity) {
return PluginEntity.wrap(bytes24(setValue));
function toModuleEntity(bytes32 setValue) pure returns (ModuleEntity) {
return ModuleEntity.wrap(bytes24(setValue));
}

// ExecutionHook layout:
// 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF______________________ Hook Plugin Entity
// 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF______________________ Hook Module Entity
// 0x________________________________________________AA____________________ is pre hook
// 0x__________________________________________________BB__________________ is post hook

function toSetValue(ExecutionHook memory executionHook) pure returns (bytes32) {
return bytes32(PluginEntity.unwrap(executionHook.hookFunction))
| bytes32(executionHook.isPreHook ? uint256(1) << 56 : 0)
| bytes32(executionHook.isPostHook ? uint256(1) << 48 : 0);
function toSetValue(HookConfig hookConfig) pure returns (bytes32) {
return bytes32(HookConfig.unwrap(hookConfig));
}

function toExecutionHook(bytes32 setValue)
pure
returns (PluginEntity hookFunction, bool isPreHook, bool isPostHook)
{
hookFunction = PluginEntity.wrap(bytes24(setValue));
isPreHook = (uint256(setValue) >> 56) & 0xFF == 1;
isPostHook = (uint256(setValue) >> 48) & 0xFF == 1;
function toHookConfig(bytes32 setValue) pure returns (HookConfig) {
return HookConfig.wrap(bytes26(setValue));
}

function toSetValue(bytes4 selector) pure returns (bytes32) {
Expand All @@ -99,12 +88,12 @@ function toSelector(bytes32 setValue) pure returns (bytes4) {
}

/// @dev Helper function to get all elements of a set into memory.
function toPluginEntityArray(EnumerableSet.Bytes32Set storage set) view returns (PluginEntity[] memory) {
function toModuleEntityArray(EnumerableSet.Bytes32Set storage set) view returns (ModuleEntity[] memory) {
uint256 length = set.length();
PluginEntity[] memory result = new PluginEntity[](length);
ModuleEntity[] memory result = new ModuleEntity[](length);
for (uint256 i = 0; i < length; ++i) {
bytes32 key = set.at(i);
result[i] = PluginEntity.wrap(bytes24(key));
result[i] = ModuleEntity.wrap(bytes24(key));
}
return result;
}
Loading

0 comments on commit d8b0207

Please sign in to comment.