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/native safe recovery module #24

Merged
merged 5 commits into from
Aug 8, 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
4 changes: 2 additions & 2 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ ignored_warnings_from = [
[rpc_endpoints]
sepolia = "${BASE_SEPOLIA_RPC_URL}"

# [etherscan]
# sepolia = { key = "${BASE_SCAN_API_KEY}" }
[etherscan]
sepolia = { key = "${BASE_SCAN_API_KEY}" }

[fmt]
bracket_spacing = true
Expand Down
19 changes: 19 additions & 0 deletions script/ComputeSafeRecoveryCalldata.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import { Script } from "forge-std/Script.sol";
import { console } from "forge-std/console.sol";

contract ComputeSafeRecoveryCalldataScript is Script {
function run() public {
address oldOwner = vm.envAddress("OLD_OWNER");
address newOwner = vm.envAddress("NEW_OWNER");
address previousOwnerInLinkedList = address(1);

bytes memory recoveryCalldata = abi.encodeWithSignature(
"swapOwner(address,address,address)", previousOwnerInLinkedList, oldOwner, newOwner
);

console.log("recoveryCalldata", vm.toString(recoveryCalldata));
}
}
51 changes: 51 additions & 0 deletions script/DeploySafeNativeRecovery.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import { Script } from "forge-std/Script.sol";
import { console } from "forge-std/console.sol";
import { Verifier } from "ether-email-auth/packages/contracts/src/utils/Verifier.sol";
import { ECDSAOwnedDKIMRegistry } from
"ether-email-auth/packages/contracts/src/utils/ECDSAOwnedDKIMRegistry.sol";
import { EmailAuth } from "ether-email-auth/packages/contracts/src/EmailAuth.sol";
import { SafeRecoverySubjectHandler } from "src/handlers/SafeRecoverySubjectHandler.sol";
import { SafeEmailRecoveryModule } from "src/modules/SafeEmailRecoveryModule.sol";

contract DeploySafeNativeRecovery_Script is Script {
function run() public {
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
address verifier = vm.envOr("VERIFIER", address(0));
address dkimRegistry = vm.envOr("DKIM_REGISTRY", address(0));
address dkimRegistrySigner = vm.envOr("SIGNER", address(0));
address emailAuthImpl = vm.envOr("EMAIL_AUTH_IMPL", address(0));
address subjectHandler = vm.envOr("SUBJECT_HANDLER", address(0));

if (verifier == address(0)) {
verifier = address(new Verifier());
console.log("Deployed Verifier at", verifier);
}

if (dkimRegistry == address(0)) {
require(dkimRegistrySigner != address(0), "DKIM_REGISTRY_SIGNER is required");
dkimRegistry = address(new ECDSAOwnedDKIMRegistry(dkimRegistrySigner));
console.log("Deployed DKIM Registry at", dkimRegistry);
}

if (emailAuthImpl == address(0)) {
emailAuthImpl = address(new EmailAuth());
console.log("Deployed Email Auth at", emailAuthImpl);
}

if (subjectHandler == address(0)) {
subjectHandler = address(new SafeRecoverySubjectHandler());
console.log("Deployed Subject Handler at", subjectHandler);
}

address module = address(
new SafeEmailRecoveryModule(verifier, dkimRegistry, emailAuthImpl, subjectHandler)
);

console.log("Deployed Email Recovery Module at ", vm.toString(module));

vm.stopBroadcast();
}
}
2 changes: 1 addition & 1 deletion src/EmailRecoveryManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ abstract contract EmailRecoveryManager is
uint256 delay,
uint256 expiry
)
internal
public
{
address account = msg.sender;

Expand Down
17 changes: 17 additions & 0 deletions src/interfaces/ISafe.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,21 @@ interface ISafe {
function getOwners() external view returns (address[] memory);
function setFallbackHandler(address handler) external;
function setGuard(address guard) external;
function execTransactionFromModule(
address to,
uint256 value,
bytes memory data,
uint8 operation
)
external
returns (bool success);
function execTransactionFromModuleReturnData(
address to,
uint256 value,
bytes memory data,
uint8 operation
)
external
returns (bool success, bytes memory returnData);
function isModuleEnabled(address module) external view returns (bool);
}
74 changes: 74 additions & 0 deletions src/modules/SafeEmailRecoveryModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import { ISafe } from "../interfaces/ISafe.sol";
import { Enum } from "@safe-global/safe-contracts/contracts/common/Enum.sol";
import { EmailRecoveryManager } from "../EmailRecoveryManager.sol";

/**
* A safe module that recovers a safe owner via ZK Email
*/
contract SafeEmailRecoveryModule is EmailRecoveryManager {
bytes4 public constant selector = bytes4(keccak256(bytes("swapOwner(address,address,address)")));

event RecoveryExecuted(address indexed account);

error InvalidAccount(address account);
error InvalidSelector(bytes4 selector);
error RecoveryFailed(address account);

constructor(
address verifier,
address dkimRegistry,
address emailAuthImpl,
address subjectHandler
)
EmailRecoveryManager(verifier, dkimRegistry, emailAuthImpl, subjectHandler)
{ }

/**
* Check if a recovery request can be initiated based on guardian acceptance
* @param account The smart account to check
* @return true if the recovery request can be started, false otherwise
*/
function canStartRecoveryRequest(address account) external view returns (bool) {
GuardianConfig memory guardianConfig = getGuardianConfig(account);

return guardianConfig.acceptedWeight >= guardianConfig.threshold;
}

/**
* @notice Executes recovery on a Safe account. Called from the recovery manager
* @param account The account to execute recovery for
* @param recoveryData The recovery data that should be executed on the Safe
* being recovered. recoveryData = abi.encode(safeAccount, recoveryFunctionCalldata)
*/
function recover(address account, bytes calldata recoveryData) internal override {
(address encodedAccount, bytes memory recoveryCalldata) =
abi.decode(recoveryData, (address, bytes));

if (encodedAccount == address(0) || encodedAccount != account) {
revert InvalidAccount(encodedAccount);
}

bytes4 calldataSelector;
assembly {
calldataSelector := mload(add(recoveryCalldata, 32))
}
if (calldataSelector != selector) {
revert InvalidSelector(calldataSelector);
}

bool success = ISafe(account).execTransactionFromModule({
to: account,
value: 0,
data: recoveryCalldata,
operation: uint8(Enum.Operation.Call)
});
if (!success) {
revert RecoveryFailed(account);
}

emit RecoveryExecuted(account);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ contract OwnableValidatorRecovery_EmailRecoveryModule_Integration_Test is
assertEq(recoveryRequest.executeAfter, 0);
assertEq(recoveryRequest.executeBefore, 0);
assertEq(recoveryRequest.currentWeight, 1);
assertEq(recoveryRequest.recoveryDataHash, recoveryDataHash1);

// handle recovery request for guardian 2
uint256 executeAfter = block.timestamp + delay;
Expand All @@ -76,6 +77,7 @@ contract OwnableValidatorRecovery_EmailRecoveryModule_Integration_Test is
assertEq(recoveryRequest.executeAfter, executeAfter);
assertEq(recoveryRequest.executeBefore, executeBefore);
assertEq(recoveryRequest.currentWeight, 3);
assertEq(recoveryRequest.recoveryDataHash, recoveryDataHash1);

// Time travel so that the recovery delay has passed
vm.warp(block.timestamp + delay);
Expand All @@ -89,6 +91,7 @@ contract OwnableValidatorRecovery_EmailRecoveryModule_Integration_Test is
assertEq(recoveryRequest.executeAfter, 0);
assertEq(recoveryRequest.executeBefore, 0);
assertEq(recoveryRequest.currentWeight, 0);
assertEq(recoveryRequest.recoveryDataHash, bytes32(0));
assertEq(updatedOwner, newOwner1);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ contract OwnableValidatorRecovery_UniversalEmailRecoveryModule_Integration_Test
assertEq(recoveryRequest.executeAfter, 0);
assertEq(recoveryRequest.executeBefore, 0);
assertEq(recoveryRequest.currentWeight, 1);
assertEq(recoveryRequest.recoveryDataHash, recoveryDataHash1);

// handle recovery request for guardian 2
uint256 executeAfter = block.timestamp + delay;
Expand All @@ -75,6 +76,7 @@ contract OwnableValidatorRecovery_UniversalEmailRecoveryModule_Integration_Test
assertEq(recoveryRequest.executeAfter, executeAfter);
assertEq(recoveryRequest.executeBefore, executeBefore);
assertEq(recoveryRequest.currentWeight, 3);
assertEq(recoveryRequest.recoveryDataHash, recoveryDataHash1);

// Time travel so that the recovery delay has passed
vm.warp(block.timestamp + delay);
Expand All @@ -88,6 +90,7 @@ contract OwnableValidatorRecovery_UniversalEmailRecoveryModule_Integration_Test
assertEq(recoveryRequest.executeAfter, 0);
assertEq(recoveryRequest.executeBefore, 0);
assertEq(recoveryRequest.currentWeight, 0);
assertEq(recoveryRequest.recoveryDataHash, bytes32(0));
assertEq(updatedOwner, newOwner1);
}

Expand Down
Loading