Skip to content

Commit

Permalink
Merge pull request #24 from zkemail/feat/native-safe-recovery-module
Browse files Browse the repository at this point in the history
Feat/native safe recovery module
  • Loading branch information
JohnGuilding committed Aug 8, 2024
2 parents 43e4efe + 1e7da8c commit 2b716bd
Show file tree
Hide file tree
Showing 13 changed files with 503 additions and 35 deletions.
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

0 comments on commit 2b716bd

Please sign in to comment.