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

ERC 7779 support #231

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
Open
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
40 changes: 25 additions & 15 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 All @@ -44,6 +45,7 @@ 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";

Expand All @@ -55,7 +57,7 @@ import { Initializable } from "./lib/Initializable.sol";
/// @author @filmakarov | Biconomy | [email protected]
/// @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 bytes;
using NonceLib for uint256;
Expand Down Expand Up @@ -120,8 +122,12 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra
} else {
// If the account is not initialized, check the signature against the account
if (!_isAlreadyInitialized()) {
// Check the userOp signature if the validator is not installed (used for EIP7702)
validationData = _checkUserOpSignature(op.signature, userOpHash);
if (ECDSA.recover(userOpHash.toEthSignedMessageHash(), op.signature) == address(this)) {
// add 7739 storage base
validationData = VALIDATION_SUCCESS;
} else {
validationData = VALIDATION_FAILED;
}
} else {
// If the account is initialized, revert as the validator is not installed
revert ValidatorNotInstalled(validator);
Expand Down Expand Up @@ -263,7 +269,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 @@ -413,17 +423,17 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra
/// @param newImplementation The address of the new implementation to upgrade to.
function _authorizeUpgrade(address newImplementation) internal virtual override(UUPSUpgradeable) onlyEntryPointOrSelf { }

/// @dev Checks if the userOp signer matches address(this), returns VALIDATION_SUCCESS if it does, otherwise VALIDATION_FAILED
/// @param signature The signature to check.
/// @param userOpHash The hash of the user operation data.
/// @return The validation result.
function _checkUserOpSignature(bytes calldata signature, bytes32 userOpHash) internal view returns (uint256) {
// Recover the signer from the signature, if it is the account, return success, otherwise revert
address signer = ECDSA.recover(userOpHash.toEthSignedMessageHash(), signature);
if (signer == address(this)) {
return VALIDATION_SUCCESS;
}
return VALIDATION_FAILED;
/// @dev This function is called when the account is redelegated.
function _onRedelegation() internal virtual override {
AccountStorage storage $ = _getAccountStorage();

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

// reinitialize the module manager
_initModuleManager();
}

/// @dev EIP712 domain name and version.
Expand Down
10 changes: 10 additions & 0 deletions contracts/base/BaseAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,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;
}
41 changes: 40 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: [email protected]

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 @@ -194,6 +194,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 @@ -212,6 +227,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 @@ -231,6 +259,17 @@ 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 _tryUninstallHook() internal {
address hook = _getHook();
if (hook != address(0)) {
try IHook(hook).onUninstall("") {} catch (bytes memory reason) {
emit HookUninstallFailed(hook, "", reason);
}
_setHook(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 | [email protected]
/// @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
}

}
27 changes: 26 additions & 1 deletion test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { NexusTest_Base } from "../../../utils/NexusTest_Base.t.sol";
import "../../../utils/Imports.sol";
import { MockTarget } from "contracts/mocks/MockTarget.sol";
import { IExecutionHelper } from "contracts/interfaces/base/IExecutionHelper.sol";
import { IHook } from "contracts/interfaces/modules/IHook.sol";

contract TestEIP7702 is NexusTest_Base {
using ECDSA for bytes32;
Expand Down Expand Up @@ -235,4 +236,28 @@ contract TestEIP7702 is NexusTest_Base {
// Assert that the value was set ie that execution was successful
assertTrue(valueTarget.balance == value);
}
}

function test_erc7702_redelegate() public {
address account = test_initializeAndExecSingle();
assertTrue(INexus(account).isModuleInstalled(MODULE_TYPE_VALIDATOR, address(mockValidator), ""));
assertTrue(INexus(account).isModuleInstalled(MODULE_TYPE_EXECUTOR, address(mockExecutor), ""));

// storage is cleared
vm.prank(address(account));
INexus(account).onRedelegation();
assertFalse(INexus(account).isModuleInstalled(MODULE_TYPE_VALIDATOR, address(mockValidator), ""));
assertFalse(INexus(account).isModuleInstalled(MODULE_TYPE_EXECUTOR, address(mockExecutor), ""));

// account is properly initialized to install modules again
vm.startPrank(address(ENTRYPOINT));
INexus(account).installModule(MODULE_TYPE_VALIDATOR, address(mockValidator), "");
INexus(account).installModule(MODULE_TYPE_EXECUTOR, address(mockExecutor), "");
INexus(account).installModule(MODULE_TYPE_HOOK, address(HOOK_MODULE), "");

vm.stopPrank();
assertTrue(INexus(account).isModuleInstalled(MODULE_TYPE_VALIDATOR, address(mockValidator), ""));
assertTrue(INexus(account).isModuleInstalled(MODULE_TYPE_EXECUTOR, address(mockExecutor), ""));
assertTrue(INexus(account).isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""));
}

}
35 changes: 35 additions & 0 deletions test/foundry/unit/fuzz/TestFuzz_ERC7779Adapter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import { MockERC7779 } from "contracts/mocks/MockERC7779.sol";
import "forge-std/Test.sol";

/// @title TestFuzz_ERC7779Adapter
/// @notice Tests the ERC7779Adapter contract
contract TestFuzz_ERC7779Adapter is Test {
MockERC7779 private mockERC7779;

function setUp() public {
mockERC7779 = new MockERC7779();
//bytes32 erc7779StorageBase = keccak256(abi.encode(uint256(keccak256(bytes("InteroperableDelegatedAccount.ERC.Storage"))) - 1)) & ~bytes32(uint256(0xff));
//console.logBytes32(erc7779StorageBase);
}

function test_Fuzz_ERC7779Adapter_AddStorageBases(uint256 amountOfBases) public {
vm.assume(amountOfBases > 0 && amountOfBases < 100);
bytes32[] memory expectedStorageBases = new bytes32[](amountOfBases);

for (uint256 i = 0; i < amountOfBases; i++) {
bytes32 storageBase = bytes32(uint256(i));
expectedStorageBases[i] = storageBase;
mockERC7779.addStorageBase(storageBase);
}

bytes32[] memory retrievedStorageBases = mockERC7779.accountStorageBases();
assertEq(retrievedStorageBases.length, amountOfBases);
for (uint256 i = 0; i < amountOfBases; i++) {
assertEq(retrievedStorageBases[i], expectedStorageBases[i]);
}
}
}

Loading