Skip to content

Commit

Permalink
Merge pull request #231 from bcnmy/feat/erc-7779-support
Browse files Browse the repository at this point in the history
ERC-7779 support v 0.1
  • Loading branch information
filmakarov authored Feb 6, 2025
2 parents f66d85b + 40630e8 commit d8078a9
Show file tree
Hide file tree
Showing 11 changed files with 316 additions and 8 deletions.
27 changes: 25 additions & 2 deletions contracts/Nexus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { IERC7484 } from "./interfaces/IERC7484.sol";
import { ModuleManager } from "./base/ModuleManager.sol";
import { ExecutionHelper } from "./base/ExecutionHelper.sol";
import { IValidator } from "./interfaces/modules/IValidator.sol";
import { IHook } from "./interfaces/modules/IHook.sol";
import {
MODULE_TYPE_VALIDATOR,
MODULE_TYPE_EXECUTOR,
Expand Down Expand Up @@ -50,6 +51,8 @@ import {
} from "./lib/ModeLib.sol";
import { NonceLib } from "./lib/NonceLib.sol";
import { SentinelListLib, SENTINEL, ZERO_ADDRESS } from "sentinellist/SentinelList.sol";
import { ERC7779Adapter } from "./base/ERC7779Adapter.sol";
import { ECDSA } from "solady/utils/ECDSA.sol";
import { Initializable } from "./lib/Initializable.sol";
import { EmergencyUninstall, Execution } from "./types/DataTypes.sol";

Expand All @@ -61,7 +64,7 @@ import { EmergencyUninstall, Execution } from "./types/DataTypes.sol";
/// @author @filmakarov | Biconomy | filipp.makarov@biconomy.io
/// @author @zeroknots | Rhinestone.wtf | zeroknots.eth
/// Special thanks to the Solady team for foundational contributions: https://github.com/Vectorized/solady
contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgradeable {
contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgradeable, ERC7779Adapter {
using ModeLib for ExecutionMode;
using ExecLib for *;
using NonceLib for uint256;
Expand Down Expand Up @@ -126,7 +129,9 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra
(userOpHash, userOp.signature) = _withPreValidationHook(userOpHash, op, missingAccountFunds);
validationData = IValidator(validator).validateUserOp(userOp, userOpHash);
} else {
// add 7739 storage base
validationData = _eip7702SignatureValidation(userOpHash, op.signature, validator) ? VALIDATION_SUCCESS : VALIDATION_FAILED;

}
}
}
Expand Down Expand Up @@ -298,7 +303,11 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra

_initModuleManager();
(address bootstrap, bytes memory bootstrapCall) = abi.decode(initData, (address, bytes));
(bool success,) = bootstrap.delegatecall(bootstrapCall);
(bool success, ) = bootstrap.delegatecall(bootstrapCall);

if (_amIERC7702()) {
_addStorageBase(_NEXUS_STORAGE_LOCATION);
}

require(success, NexusInitializationFailed());
require(_hasValidators(), NoValidatorInstalled());
Expand Down Expand Up @@ -514,6 +523,20 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra
/// @dev Ensures that only authorized callers can upgrade the smart contract implementation.
/// This is part of the UUPS (Universal Upgradeable Proxy Standard) pattern.
/// @param newImplementation The address of the new implementation to upgrade to.

/// @dev This function is called when the account is redelegated.
function _onRedelegation() internal virtual override {
AccountStorage storage $ = _getAccountStorage();

_tryUninstallValidators();
_tryUninstallExecutors();
$.emergencyUninstallTimelock[address($.hook)] = 0;
_tryUninstallHooks();

// account should be properly initialized for the new delegate
// use Nexus.initializeAccount() to reinitialize the account
// otherwise modules will not be installed as the module manager is not initialized

function _authorizeUpgrade(address newImplementation) internal virtual override(UUPSUpgradeable) {
_onlyEntryPointOrSelf();
}
Expand Down
10 changes: 10 additions & 0 deletions contracts/base/BaseAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,14 @@ contract BaseAccount is IBaseAccount {
function entryPoint() external view returns (address) {
return _ENTRYPOINT;
}

function _amIERC7702() internal view returns (bool res) {
assembly {
res :=
eq(
extcodehash(address()),
0xeadcdba66a79ab5dce91622d1d75c8cff5cff0b96944c3bf1072cd08ce018329 // (keccak256(0xef01))
)
}
}
}
62 changes: 62 additions & 0 deletions contracts/base/ERC7779Adapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import { IERC7779 } from "../interfaces/IERC7779.sol";

abstract contract ERC7779Adapter is IERC7779 {
error NonAuthorizedOnRedelegationCaller();

// keccak256(abi.encode(uint256(keccak256(bytes("InteroperableDelegatedAccount.ERC.Storage"))) - 1)) & ~bytes32(uint256(0xff));
bytes32 internal constant ERC7779_STORAGE_BASE = 0xc473de86d0138e06e4d4918a106463a7cc005258d2e21915272bcb4594c18900;

struct ERC7779Storage {
bytes32[] storageBases;
}

/*
* @dev Externally shares the storage bases that has been used throughout the account.
* Majority of 7702 accounts will have their distinctive storage base to reduce the
chance of storage collision.
* This allows the external entities to know what the storage base is of the account.
* Wallets willing to redelegate already-delegated accounts should call
accountStorageBase() to check if it confirms with the account it plans to redelegate.
*
* The bytes32 array should be stored at the storage slot:
keccak(keccak('InteroperableDelegatedAccount.ERC.Storage')-1) & ~0xff
* This is an append-only array so newly redelegated accounts should not overwrite the
storage at this slot, but just append their base to the array.
* This append operation should be done during the initialization of the account.
*/
function accountStorageBases() external view returns (bytes32[] memory) {
ERC7779Storage storage $;
assembly {
$.slot := ERC7779_STORAGE_BASE
}
return $.storageBases;
}

function _addStorageBase(bytes32 storageBase) internal {
ERC7779Storage storage $;
assembly {
$.slot := ERC7779_STORAGE_BASE
}
$.storageBases.push(storageBase);
}

/*
* @dev Function called before redelegation.
* This function should prepare the account for a delegation to a different implementation.
* This function could be triggered by the new wallet that wants to redelegate an already delegated EOA.
* It should uninitialize storages if needed and execute wallet-specific logic to prepare for redelegation.
* msg.sender should be the owner of the account.
*/
function onRedelegation() external returns (bool) {
require(msg.sender == address(this), NonAuthorizedOnRedelegationCaller());
_onRedelegation();
return true;
}

/// @dev This function is called when the account is redelegated.
/// @dev This function should be overridden by the account to implement wallet-specific logic.
function _onRedelegation() internal virtual;
}
74 changes: 73 additions & 1 deletion contracts/base/ModuleManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pragma solidity ^0.8.27;
// Nexus: A suite of contracts for Modular Smart Accounts compliant with ERC-7579 and ERC-4337, developed by Biconomy.
// Learn more at https://biconomy.io. To report security issues, please contact us at: security@biconomy.io

import { SentinelListLib } from "sentinellist/SentinelList.sol";
import { SentinelListLib, SENTINEL } from "sentinellist/SentinelList.sol";
import { Storage } from "./Storage.sol";
import { IHook } from "../interfaces/modules/IHook.sol";
import { IModule } from "../interfaces/modules/IModule.sol";
Expand Down Expand Up @@ -113,6 +113,25 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError
return _getHook();
}

/// @notice Checks if a nonce has been used.
/// @param nonce The nonce to check.
/// @return bool True if the nonce has been used, false otherwise.
function wasNonceUsed(uint256 nonce) external view returns (bool) {
return _getAccountStorage().nonces[nonce];
}

/// @notice Fetches the 4337 pre-validation hook.
/// @return hook The address of the 4337 pre-validation hook.
function get4337PreValidationHook() external view returns (address) {
return address(_getAccountStorage().preValidationHookERC4337);
}

/// @notice Fetches the 1271 pre-validation hook.
/// @return hook The address of the 1271 pre-validation hook.
function get1271PreValidationHook() external view returns (address) {
return address(_getAccountStorage().preValidationHookERC1271);
}

/// @notice Fetches the fallback handler for a specific selector.
/// @param selector The function selector to query.
/// @return calltype The type of call that the handler manages.
Expand Down Expand Up @@ -207,6 +226,21 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError
validator.excessivelySafeCall(gasleft(), 0, 0, abi.encodeWithSelector(IModule.onUninstall.selector, disableModuleData));
}

/// @dev Uninstalls all validators and emits an event if any validator fails to uninstall.
function _tryUninstallValidators() internal {
SentinelListLib.SentinelList storage validators = _getAccountStorage().validators;
address validator = validators.getNext(SENTINEL);
// we do not need excessivelySafeCall here as it prevents reversion
// we want to know if there's revert and emit the event
while (validator != SENTINEL) {
try IValidator(validator).onUninstall("") {} catch (bytes memory reason) {
emit ValidatorUninstallFailed(validator, "", reason);
}
validator = validators.getNext(validator);
}
validators.popAll();
}

/// @dev Installs a new executor module after checking if it matches the required module type.
/// @param executor The address of the executor module to be installed.
/// @param data Initialization data to configure the executor upon installation.
Expand All @@ -225,6 +259,19 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError
executor.excessivelySafeCall(gasleft(), 0, 0, abi.encodeWithSelector(IModule.onUninstall.selector, disableModuleData));
}

/// @dev Uninstalls all executors and emits an event if any executor fails to uninstall.
function _tryUninstallExecutors() internal {
SentinelListLib.SentinelList storage executors = _getAccountStorage().executors;
address executor = executors.getNext(SENTINEL);
while (executor != SENTINEL) {
try IExecutor(executor).onUninstall("") {} catch (bytes memory reason) {
emit ExecutorUninstallFailed(executor, "", reason);
}
executor = executors.getNext(executor);
}
executors.popAll();
}

/// @dev Installs a hook module, ensuring no other hooks are installed before proceeding.
/// @param hook The address of the hook to be installed.
/// @param data Initialization data to configure the hook upon installation.
Expand All @@ -249,6 +296,31 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError
hook.excessivelySafeCall(gasleft(), 0, 0, abi.encodeWithSelector(IModule.onUninstall.selector, data));
}

/// @dev Uninstalls the hook and emits an event if the hook fails to uninstall.
function _tryUninstallHooks() internal {
address hook = _getHook();
if (hook != address(0)) {
try IHook(hook).onUninstall("") {} catch (bytes memory reason) {
emit HookUninstallFailed(hook, "", reason);
}
_setHook(address(0));
}
hook = address(_getAccountStorage().preValidationHookERC1271);
if (hook != address(0)) {
try IPreValidationHookERC1271(hook).onUninstall("") {} catch (bytes memory reason) {
emit HookUninstallFailed(hook, "", reason);
}
_setPreValidationHook(MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(0));
}
hook = address(_getAccountStorage().preValidationHookERC4337);
if (hook != address(0)) {
try IPreValidationHookERC4337(hook).onUninstall("") {} catch (bytes memory reason) {
emit HookUninstallFailed(hook, "", reason);
}
_setPreValidationHook(MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(0));
}
}

/// @dev Sets the current hook in the storage to the specified address.
/// @param hook The new hook address.
function _setHook(address hook) internal virtual {
Expand Down
4 changes: 2 additions & 2 deletions contracts/base/Storage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { IStorage } from "../interfaces/base/IStorage.sol";
contract Storage is IStorage {
/// @custom:storage-location erc7201:biconomy.storage.Nexus
/// ERC-7201 namespaced via `keccak256(abi.encode(uint256(keccak256(bytes("biconomy.storage.Nexus"))) - 1)) & ~bytes32(uint256(0xff));`
bytes32 private constant _STORAGE_LOCATION = 0x0bb70095b32b9671358306b0339b4c06e7cbd8cb82505941fba30d1eb5b82f00;
bytes32 internal constant _NEXUS_STORAGE_LOCATION = 0x0bb70095b32b9671358306b0339b4c06e7cbd8cb82505941fba30d1eb5b82f00;

/// @dev Utilizes ERC-7201's namespaced storage pattern for isolated storage access. This method computes
/// the storage slot based on a predetermined location, ensuring collision-resistant storage for contract states.
Expand All @@ -34,7 +34,7 @@ contract Storage is IStorage {
/// @return $ The proxy to the `AccountStorage` struct, providing a reference to the namespaced storage slot.
function _getAccountStorage() internal pure returns (AccountStorage storage $) {
assembly {
$.slot := _STORAGE_LOCATION
$.slot := _NEXUS_STORAGE_LOCATION
}
}
}
26 changes: 26 additions & 0 deletions contracts/interfaces/IERC7779.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

interface IERC7779 {
/*
* @dev Externally shares the storage bases that has been used throughout the account.
* Majority of 7702 accounts will have their distinctive storage base to reduce the chance of storage collision.
* This allows the external entities to know what the storage base is of the account.
* Wallets willing to redelegate already-delegated accounts should call accountStorageBase() to check if it confirms with the account it plans to redelegate.
*
* The bytes32 array should be stored at the storage slot: keccak(keccak('InteroperableDelegatedAccount.ERC.Storage')-1) & ~0xff
* This is an append-only array so newly redelegated accounts should not overwrite the storage at this slot, but just append their base to the array.
* This append operation should be done during the initialization of the account.
*/
function accountStorageBases() external view returns (bytes32[] memory);

/*
* @dev Function called before redelegation.
* This function should prepare the account for a delegation to a different implementation.
* This function could be triggered by the new wallet that wants to redelegate an already delegated EOA.
* It should uninitialize storages if needed and execute wallet-specific logic to prepare for redelegation.
* msg.sender should be the owner of the account.
*/
function onRedelegation() external returns (bool);

}
4 changes: 2 additions & 2 deletions contracts/interfaces/INexus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pragma solidity ^0.8.27;
import { IERC4337Account } from "./IERC4337Account.sol";
import { IERC7579Account } from "./IERC7579Account.sol";
import { INexusEventsAndErrors } from "./INexusEventsAndErrors.sol";

import { IERC7779 } from "./IERC7779.sol";
/// @title Nexus - INexus Interface
/// @notice Integrates ERC-4337 and ERC-7579 standards to manage smart accounts within the Nexus suite.
/// @dev Consolidates ERC-4337 user operations and ERC-7579 configurations into a unified interface for smart account management.
Expand All @@ -27,7 +27,7 @@ import { INexusEventsAndErrors } from "./INexusEventsAndErrors.sol";
/// @author @filmakarov | Biconomy | filipp.makarov@biconomy.io
/// @author @zeroknots | Rhinestone.wtf | zeroknots.eth
/// Special thanks to the Solady team for foundational contributions: https://github.com/Vectorized/solady
interface INexus is IERC4337Account, IERC7579Account, INexusEventsAndErrors {
interface INexus is IERC4337Account, IERC7579Account, INexusEventsAndErrors, IERC7779 {
/// @notice Initializes the smart account with a validator and custom data.
/// @dev This method sets up the account for operation, linking it with a validator and initializing it with specific data.
/// Can be called directly or via a factory.
Expand Down
4 changes: 4 additions & 0 deletions contracts/interfaces/base/IModuleManagerEventsAndErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ interface IModuleManagerEventsAndErrors {
/// @param module The address of the uninstalled module.
event ModuleUninstalled(uint256 moduleTypeId, address module);

event ExecutorUninstallFailed(address executor, bytes data, bytes reason);
event ValidatorUninstallFailed(address validator, bytes data, bytes reason);
event HookUninstallFailed(address hook, bytes data, bytes reason);

/// @notice Thrown when attempting to remove the last validator.
error CanNotRemoveLastValidator();

Expand Down
16 changes: 16 additions & 0 deletions contracts/mocks/MockERC7779.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import { ERC7779Adapter } from "../base/ERC7779Adapter.sol";

contract MockERC7779 is ERC7779Adapter {

function addStorageBase(bytes32 storageBase) external {
_addStorageBase(storageBase);
}

function _onRedelegation() internal override {
// do nothing
}

}
Loading

0 comments on commit d8078a9

Please sign in to comment.