Skip to content

Commit

Permalink
Use latest manager with native safe module
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnGuilding committed Aug 7, 2024
1 parent 98388fd commit 4197d1e
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 116 deletions.
37 changes: 32 additions & 5 deletions script/DeploySafeNativeRecovery.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,46 @@ 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 manager = vm.envOr("VERIFIER", address(0));
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 (manager == address(0)) {
manager = address(1);
console.log("Deployed Manager at", manager);
if (verifier == address(0)) {
verifier = address(new Verifier());
console.log("Deployed Verifier at", verifier);
}

address module = address(new SafeEmailRecoveryModule(manager));
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));

Expand Down
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
53 changes: 24 additions & 29 deletions src/modules/SafeEmailRecoveryModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,59 +3,54 @@ 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 {
contract SafeEmailRecoveryModule is EmailRecoveryManager {
bytes4 public constant selector = bytes4(keccak256(bytes("swapOwner(address,address,address)")));

/**
* Trusted email recovery manager contract that handles recovery requests
*/
address public immutable emailRecoveryManager;

event RecoveryExecuted(address indexed account);

error ModuleEnabledCheckFailed(address account, address module);
error NotTrustedRecoveryManager();
error InvalidSelector(bytes4 selector);
error RecoveryFailed(address account);

constructor(address _emailRecoveryManager) {
emailRecoveryManager = _emailRecoveryManager;
}
constructor(
address verifier,
address dkimRegistry,
address emailAuthImpl,
address subjectHandler
)
EmailRecoveryManager(verifier, dkimRegistry, emailAuthImpl, subjectHandler)
{ }

/**
* Check if the recovery module is authorized to recover the account
* Check if a recovery request can be initiated based on guardian acceptance
* @param account The smart account to check
* @return true if the module is authorized, false otherwise
* @return true if the recovery request can be started, false otherwise
*/
function isAuthorizedToRecover(address account) external returns (bool) {
(bool success, bytes memory returnData) = ISafe(account).execTransactionFromModuleReturnData({
to: account,
value: 0,
data: abi.encodeWithSignature("isModuleEnabled(address)", address(this)),
operation: uint8(Enum.Operation.Call)
});
if (!success) {
revert ModuleEnabledCheckFailed(account, address(this));
}
return abi.decode(returnData, (bool));
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. Must be called by the trusted recovery manager
* @param account The account to execute recovery for
* @param recoveryCalldata The recovery calldata that should be executed on the Safe
* @param recoveryData The recovery calldata that should be executed on the Safe
* being recovered
*/
function recover(address account, bytes calldata recoveryCalldata) public {
if (msg.sender != emailRecoveryManager) {
revert NotTrustedRecoveryManager();
function recover(address account, bytes calldata recoveryData) internal override {
(, bytes memory recoveryCalldata) = abi.decode(recoveryData, (address, bytes));
// FIXME: What if you use this module with a different subject handler? It could chose
// not to encode the account/validator along with the calldata
bytes4 calldataSelector;
assembly {
calldataSelector := mload(add(recoveryCalldata, 32))
}

bytes4 calldataSelector = bytes4(recoveryCalldata[:4]);
if (calldataSelector != selector) {
revert InvalidSelector(calldataSelector);
}
Expand Down
50 changes: 16 additions & 34 deletions test/integration/SafeRecovery/SafeNativeIntegrationBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { Safe } from "@safe-global/safe-contracts/contracts/Safe.sol";
import { SafeProxy } from "@safe-global/safe-contracts/contracts/proxies/SafeProxy.sol";
import { SafeEmailRecoveryModule } from "src/modules/SafeEmailRecoveryModule.sol";
import { EmailRecoveryManager } from "src/EmailRecoveryManager.sol";
import { UniversalEmailRecoveryModule } from "src/modules/UniversalEmailRecoveryModule.sol";
import { SafeRecoverySubjectHandler } from "src/handlers/SafeRecoverySubjectHandler.sol";
import { IntegrationBase } from "../IntegrationBase.t.sol";

Expand All @@ -22,9 +20,8 @@ abstract contract SafeNativeIntegrationBase is IntegrationBase {
using Strings for uint256;
using Strings for address;

EmailRecoveryManager emailRecoveryManager;
address emailRecoveryManagerAddress;
SafeEmailRecoveryModule safeEmailRecoveryModule;
SafeEmailRecoveryModule emailRecoveryModule;
address emailRecoveryModuleAddress;
Safe public safeSingleton;
Safe public safe;
address public safeAddress;
Expand Down Expand Up @@ -61,53 +58,38 @@ abstract contract SafeNativeIntegrationBase is IntegrationBase {
super.setUp();

subjectHandler = address(new SafeRecoverySubjectHandler());
emailRecoveryManager = new EmailRecoveryManager(
emailRecoveryModule = new SafeEmailRecoveryModule(
address(verifier),
address(ecdsaOwnedDkimRegistry),
address(dkimRegistry),
address(emailAuthImpl),
address(subjectHandler)
);
emailRecoveryManagerAddress = address(emailRecoveryManager);

safeEmailRecoveryModule = new SafeEmailRecoveryModule(emailRecoveryManagerAddress);
emailRecoveryManager.initialize(address(safeEmailRecoveryModule));
emailRecoveryModuleAddress = address(emailRecoveryModule);

safeSingleton = new Safe();
SafeProxy safeProxy = new SafeProxy(address(safeSingleton));
safe = Safe(payable(address(safeProxy)));
safeAddress = address(safe);
// safe4337Module = new Safe4337Module(entryPointAddress);
// safeModuleSetup = new SafeModuleSetup();

isInstalledContext = bytes("0");
functionSelector = bytes4(keccak256(bytes("swapOwner(address,address,address)")));

// Compute guardian addresses
guardians1 = new address[](3);
guardians1[0] = emailRecoveryManager.computeEmailAuthAddress(safeAddress, accountSalt1);
guardians1[1] = emailRecoveryManager.computeEmailAuthAddress(safeAddress, accountSalt2);
guardians1[2] = emailRecoveryManager.computeEmailAuthAddress(safeAddress, accountSalt3);
guardians1[0] = emailRecoveryModule.computeEmailAuthAddress(safeAddress, accountSalt1);
guardians1[1] = emailRecoveryModule.computeEmailAuthAddress(safeAddress, accountSalt2);
guardians1[2] = emailRecoveryModule.computeEmailAuthAddress(safeAddress, accountSalt3);

address[] memory owners = new address[](1);
owner = owner1;
owners[0] = owner;

safe.setup(
owners,
1,
address(0),
bytes("0"),
address(0),
// address(safeModuleSetup),
// abi.encodeCall(SafeModuleSetup.enableModules, (modules)),
// address(safe4337Module),
address(0),
0,
payable(address(0))
owners, 1, address(0), bytes("0"), address(0), address(0), 0, payable(address(0))
);

vm.startPrank(safeAddress);
safe.enableModule(address(safeEmailRecoveryModule));
safe.enableModule(address(emailRecoveryModule));
vm.stopPrank();
}

Expand Down Expand Up @@ -157,7 +139,7 @@ abstract contract SafeNativeIntegrationBase is IntegrationBase {

function acceptGuardian(address account, address guardian) public {
EmailAuthMsg memory emailAuthMsg = getAcceptanceEmailAuthMessage(account, guardian);
emailRecoveryManager.handleAcceptance(emailAuthMsg, templateIdx);
emailRecoveryModule.handleAcceptance(emailAuthMsg, templateIdx);
}

function getAcceptanceEmailAuthMessage(
Expand All @@ -177,7 +159,7 @@ abstract contract SafeNativeIntegrationBase is IntegrationBase {
bytes[] memory subjectParamsForAcceptance = new bytes[](1);
subjectParamsForAcceptance[0] = abi.encode(account);
return EmailAuthMsg({
templateId: emailRecoveryManager.computeAcceptanceTemplateId(templateIdx),
templateId: emailRecoveryModule.computeAcceptanceTemplateId(templateIdx),
subjectParams: subjectParamsForAcceptance,
skipedSubjectPrefix: 0,
proof: emailProof
Expand All @@ -194,7 +176,7 @@ abstract contract SafeNativeIntegrationBase is IntegrationBase {
{
EmailAuthMsg memory emailAuthMsg =
getRecoveryEmailAuthMessage(account, oldOwner, newOwner, guardian);
emailRecoveryManager.handleRecovery(emailAuthMsg, templateIdx);
emailRecoveryModule.handleRecovery(emailAuthMsg, templateIdx);
}

function getRecoveryEmailAuthMessage(
Expand All @@ -210,7 +192,7 @@ abstract contract SafeNativeIntegrationBase is IntegrationBase {
string memory oldOwnerString = SubjectUtils.addressToChecksumHexString(oldOwner);
string memory newOwnerString = SubjectUtils.addressToChecksumHexString(newOwner);
string memory recoveryModuleString =
SubjectUtils.addressToChecksumHexString(address(safeEmailRecoveryModule));
SubjectUtils.addressToChecksumHexString(address(emailRecoveryModule));

string memory subject = string.concat(
"Recover account ",
Expand All @@ -231,10 +213,10 @@ abstract contract SafeNativeIntegrationBase is IntegrationBase {
subjectParamsForRecovery[0] = abi.encode(account);
subjectParamsForRecovery[1] = abi.encode(oldOwner);
subjectParamsForRecovery[2] = abi.encode(newOwner);
subjectParamsForRecovery[3] = abi.encode(address(safeEmailRecoveryModule));
subjectParamsForRecovery[3] = abi.encode(emailRecoveryModuleAddress);

return EmailAuthMsg({
templateId: emailRecoveryManager.computeRecoveryTemplateId(templateIdx),
templateId: emailRecoveryModule.computeRecoveryTemplateId(templateIdx),
subjectParams: subjectParamsForRecovery,
skipedSubjectPrefix: 0,
proof: emailProof
Expand Down
29 changes: 14 additions & 15 deletions test/integration/SafeRecovery/SafeRecoveryNativeModule.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,35 +25,34 @@ contract SafeRecoveryNativeModule_Integration_Test is SafeNativeIntegrationBase
address newOwner = owner2;
// Configure recovery
vm.startPrank(safeAddress);
emailRecoveryManager.configureRecovery(
guardians1, guardianWeights, threshold, delay, expiry
);
emailRecoveryModule.configureRecovery(guardians1, guardianWeights, threshold, delay, expiry);
vm.stopPrank();

bytes memory recoveryCalldata = abi.encodeWithSignature(
"swapOwner(address,address,address)", address(1), owner, newOwner
);
bytes32 calldataHash = keccak256(recoveryCalldata);
bytes memory recoveryData = abi.encode(safeAddress, recoveryCalldata);
bytes32 calldataHash = keccak256(recoveryData);

bytes[] memory subjectParamsForRecovery = new bytes[](4);
subjectParamsForRecovery[0] = abi.encode(safeAddress);
subjectParamsForRecovery[1] = abi.encode(owner);
subjectParamsForRecovery[2] = abi.encode(newOwner);
subjectParamsForRecovery[3] = abi.encode(address(safeEmailRecoveryModule));
subjectParamsForRecovery[3] = abi.encode(emailRecoveryModuleAddress);

// Accept guardian
EmailAuthMsg memory emailAuthMsg = getAcceptanceEmailAuthMessage(safeAddress, guardians1[0]);
emailRecoveryManager.handleAcceptance(emailAuthMsg, templateIdx);
emailRecoveryModule.handleAcceptance(emailAuthMsg, templateIdx);
GuardianStorage memory guardianStorage1 =
emailRecoveryManager.getGuardian(safeAddress, guardians1[0]);
emailRecoveryModule.getGuardian(safeAddress, guardians1[0]);
assertEq(uint256(guardianStorage1.status), uint256(GuardianStatus.ACCEPTED));
assertEq(guardianStorage1.weight, uint256(1));

// Accept guardian
emailAuthMsg = getAcceptanceEmailAuthMessage(safeAddress, guardians1[1]);
emailRecoveryManager.handleAcceptance(emailAuthMsg, templateIdx);
emailRecoveryModule.handleAcceptance(emailAuthMsg, templateIdx);
GuardianStorage memory guardianStorage2 =
emailRecoveryManager.getGuardian(safeAddress, guardians1[1]);
emailRecoveryModule.getGuardian(safeAddress, guardians1[1]);
assertEq(uint256(guardianStorage2.status), uint256(GuardianStatus.ACCEPTED));
assertEq(guardianStorage2.weight, uint256(2));

Expand All @@ -62,27 +61,27 @@ contract SafeRecoveryNativeModule_Integration_Test is SafeNativeIntegrationBase

// handle recovery request for guardian 1
emailAuthMsg = getRecoveryEmailAuthMessage(safeAddress, owner, newOwner, guardians1[0]);
emailRecoveryManager.handleRecovery(emailAuthMsg, templateIdx);
emailRecoveryModule.handleRecovery(emailAuthMsg, templateIdx);
IEmailRecoveryManager.RecoveryRequest memory recoveryRequest =
emailRecoveryManager.getRecoveryRequest(safeAddress);
emailRecoveryModule.getRecoveryRequest(safeAddress);
assertEq(recoveryRequest.currentWeight, 1);

// handle recovery request for guardian 2
uint256 executeAfter = block.timestamp + delay;
uint256 executeBefore = block.timestamp + expiry;
emailAuthMsg = getRecoveryEmailAuthMessage(safeAddress, owner, newOwner, guardians1[1]);
emailRecoveryManager.handleRecovery(emailAuthMsg, templateIdx);
recoveryRequest = emailRecoveryManager.getRecoveryRequest(safeAddress);
emailRecoveryModule.handleRecovery(emailAuthMsg, templateIdx);
recoveryRequest = emailRecoveryModule.getRecoveryRequest(safeAddress);
assertEq(recoveryRequest.executeAfter, executeAfter);
assertEq(recoveryRequest.executeBefore, executeBefore);
assertEq(recoveryRequest.currentWeight, 3);

vm.warp(block.timestamp + delay);

// Complete recovery
emailRecoveryManager.completeRecovery(safeAddress, recoveryCalldata);
emailRecoveryModule.completeRecovery(safeAddress, recoveryData);

recoveryRequest = emailRecoveryManager.getRecoveryRequest(safeAddress);
recoveryRequest = emailRecoveryModule.getRecoveryRequest(safeAddress);
assertEq(recoveryRequest.executeAfter, 0);
assertEq(recoveryRequest.executeBefore, 0);
assertEq(recoveryRequest.currentWeight, 0);
Expand Down
Loading

0 comments on commit 4197d1e

Please sign in to comment.