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: add hook emergency uninstall #154

Merged
merged 9 commits into from
Sep 4, 2024
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
29 changes: 29 additions & 0 deletions contracts/Nexus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra
/// @dev `keccak256("PersonalSign(bytes prefixed)")`.
bytes32 internal constant _PERSONAL_SIGN_TYPEHASH = 0x983e65e5148e570cd828ead231ee759a8d7958721a768f93bc4483ba005c32de;

/// @dev The timelock period for emergency hook uninstallation.
uint256 internal constant _EMERGENCY_TIMELOCK = 1 days;

/// @dev The event emitted when an emergency hook uninstallation is initiated.
event EmergencyHookUninstallRequest(address hook, uint256 timestamp);

/// @notice Initializes the smart account with the specified entry point.
constructor(address anEntryPoint) {
require(address(anEntryPoint) != address(0), EntryPointCanNotBeZero());
Expand Down Expand Up @@ -180,6 +186,29 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra
}
}

function emergencyUninstallHook(address hook, bytes calldata deInitData) external payable onlyEntryPoint {
AccountStorage storage accountStorage = _getAccountStorage();
uint256 hookTimelock = accountStorage.emergencyUninstallTimelock[hook];

if (hookTimelock == 0) {
// if the timelock hasnt been initiated, initiate it
accountStorage.emergencyUninstallTimelock[hook] = block.timestamp;
emit EmergencyHookUninstallRequest(hook, block.timestamp);
} else if (block.timestamp >= hookTimelock + 3 * _EMERGENCY_TIMELOCK) {
// if the timelock has been left for too long, reset it
accountStorage.emergencyUninstallTimelock[hook] = block.timestamp;
emit EmergencyHookUninstallRequest(hook, block.timestamp);
} else if (block.timestamp >= hookTimelock + _EMERGENCY_TIMELOCK) {
// if the timelock expired, clear it and uninstall the hook
accountStorage.emergencyUninstallTimelock[hook] = 0;
_uninstallHook(hook, deInitData);
emit ModuleUninstalled(MODULE_TYPE_HOOK, hook);
} else {
// if the timelock is initiated but not expired, revert
revert EmergencyTimeLockNotExpired();
}
}

function initializeAccount(bytes calldata initData) external payable virtual {
_initModuleManager();
(address bootstrap, bytes memory bootstrapCall) = abi.decode(initData, (address, bytes));
Expand Down
3 changes: 3 additions & 0 deletions contracts/interfaces/INexusEventsAndErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,7 @@ interface INexusEventsAndErrors {

/// @notice Error thrown when an inner call fails.
error InnerCallFailed();

/// @notice Error thrown when attempted to emergency-uninstall a hook
error EmergencyTimeLockNotExpired();
}
1 change: 1 addition & 0 deletions contracts/interfaces/base/IStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ interface IStorage {
SentinelListLib.SentinelList executors; ///< List of executors, similarly initialized.
mapping(bytes4 => FallbackHandler) fallbacks; ///< Mapping of selectors to their respective fallback handlers.
IHook hook; ///< Current hook module associated with this account.
mapping(address hook => uint256) emergencyUninstallTimelock; ///< Mapping of hooks to requested timelocks.
}

/// @notice Defines a fallback handler with an associated handler address and a call type.
Expand Down
163 changes: 163 additions & 0 deletions test/foundry/unit/concrete/hook/TestNexus_Hook_Emergency_Uninstall.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import "../../../shared/TestModuleManagement_Base.t.sol";
import "../../../../../contracts/mocks/MockHook.sol";

/// @title TestNexus_Hook_Uninstall
/// @notice Tests for handling hooks emergency uninstall
contract TestNexus_Hook_Emergency_Uninstall is TestModuleManagement_Base {
/// @notice Sets up the base module management environment.
function setUp() public {
setUpModuleManagement_Base();
}

/// @notice Tests the successful installation of the hook module, then tests initiate emergency uninstall.
function test_EmergencyUninstallHook_Initiate_Success() public {
// 1. Install the hook

// Ensure the hook module is not installed initially
assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook module should not be installed initially");

// Prepare call data for installing the hook module
bytes memory callData = abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_HOOK, address(HOOK_MODULE), "");

// Install the hook module
installModule(callData, MODULE_TYPE_HOOK, address(HOOK_MODULE), EXECTYPE_DEFAULT);

// Assert that the hook module is now installed
assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook module should be installed");

uint256 prevTimeStamp = block.timestamp;



// 2. Request to uninstall the hook
bytes memory emergencyUninstallCalldata = abi.encodeWithSelector(Nexus.emergencyUninstallHook.selector, address(HOOK_MODULE), "");

// Initialize the userOps array with one operation
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
userOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(VALIDATOR_MODULE)));
userOps[0].callData = emergencyUninstallCalldata;
bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]);
userOps[0].signature = signMessage(BOB, userOpHash);

vm.expectEmit(true, true, true, true);
emit EmergencyHookUninstallRequest(address(HOOK_MODULE), block.timestamp);

ENTRYPOINT.handleOps(userOps, payable(BOB.addr));

assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook MUST still be installed");
}

function test_EmergencyUninstallHook_Fail_AfterInitiated() public {
// 1. Install the hook

// Ensure the hook module is not installed initially
assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook module should not be installed initially");

// Prepare call data for installing the hook module
bytes memory callData = abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_HOOK, address(HOOK_MODULE), "");

// Install the hook module
installModule(callData, MODULE_TYPE_HOOK, address(HOOK_MODULE), EXECTYPE_DEFAULT);

// Assert that the hook module is now installed
assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook module should be installed");

uint256 prevTimeStamp = block.timestamp;



// 2. Request to uninstall the hook
bytes memory emergencyUninstallCalldata = abi.encodeWithSelector(Nexus.emergencyUninstallHook.selector, address(HOOK_MODULE), "");

// Initialize the userOps array with one operation
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
userOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(VALIDATOR_MODULE)));
userOps[0].callData = emergencyUninstallCalldata;
bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]);
userOps[0].signature = signMessage(BOB, userOpHash);

vm.expectEmit(true, true, true, true);
emit EmergencyHookUninstallRequest(address(HOOK_MODULE), block.timestamp);

ENTRYPOINT.handleOps(userOps, payable(BOB.addr));


// 3. Try without waiting for time to pass
PackedUserOperation[] memory newUserOps = new PackedUserOperation[](1);
newUserOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(VALIDATOR_MODULE)));
newUserOps[0].callData = emergencyUninstallCalldata;
bytes32 newUserOpHash = ENTRYPOINT.getUserOpHash(newUserOps[0]);
newUserOps[0].signature = signMessage(BOB, newUserOpHash);

bytes memory expectedRevertReason = abi.encodeWithSelector(EmergencyTimeLockNotExpired.selector);
// Expect the UserOperationRevertReason event
vm.expectEmit(true, true, true, true);
emit UserOperationRevertReason(
newUserOpHash, // userOpHash
address(BOB_ACCOUNT), // sender
newUserOps[0].nonce, // nonce
expectedRevertReason
);
ENTRYPOINT.handleOps(newUserOps, payable(BOB.addr));

assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook MUST still be installed");
}

function test_EmergencyUninstallHook_Success_LongAfterInitiated() public {
// 1. Install the hook

// Ensure the hook module is not installed initially
assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook module should not be installed initially");

// Prepare call data for installing the hook module
bytes memory callData = abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_HOOK, address(HOOK_MODULE), "");

// Install the hook module
installModule(callData, MODULE_TYPE_HOOK, address(HOOK_MODULE), EXECTYPE_DEFAULT);

// Assert that the hook module is now installed
assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook module should be installed");

uint256 prevTimeStamp = block.timestamp;



// 2. Request to uninstall the hook
bytes memory emergencyUninstallCalldata = abi.encodeWithSelector(Nexus.emergencyUninstallHook.selector, address(HOOK_MODULE), "");

// Initialize the userOps array with one operation
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
userOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(VALIDATOR_MODULE)));
userOps[0].callData = emergencyUninstallCalldata;
bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]);
userOps[0].signature = signMessage(BOB, userOpHash);

vm.expectEmit(true, true, true, true);
emit EmergencyHookUninstallRequest(address(HOOK_MODULE), block.timestamp);

ENTRYPOINT.handleOps(userOps, payable(BOB.addr));


// 3. Wait for time to pass
// not more than 3 days
vm.warp(prevTimeStamp + 2 days);

PackedUserOperation[] memory newUserOps = new PackedUserOperation[](1);
newUserOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(VALIDATOR_MODULE)));
newUserOps[0].callData = emergencyUninstallCalldata;
bytes32 newUserOpHash = ENTRYPOINT.getUserOpHash(newUserOps[0]);
newUserOps[0].signature = signMessage(BOB, newUserOpHash);

bytes memory expectedRevertReason = abi.encodeWithSelector(EmergencyTimeLockNotExpired.selector);
// Expect the UserOperationRevertReason event
vm.expectEmit(true, true, true, true);
emit ModuleUninstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE));
ENTRYPOINT.handleOps(newUserOps, payable(BOB.addr));

assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook module should not be installed anymore");
}

}
2 changes: 2 additions & 0 deletions test/foundry/utils/EventsAndErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ contract EventsAndErrors {
event PostCheckCalled();
event TryExecuteUnsuccessful(bytes callData, bytes result);
event TryDelegateCallUnsuccessful(bytes callData, bytes result);
event EmergencyHookUninstallRequest(address hook, uint256 timestamp);

// ==========================
// General Errors
Expand Down Expand Up @@ -73,6 +74,7 @@ contract EventsAndErrors {
error EnableModeSigError();
error InvalidModule(address module);
error NoValidatorInstalled();
error EmergencyTimeLockNotExpired();

// ==========================
// Hook Errors
Expand Down
Loading