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: adding validator recovery flow #1

Closed
Closed
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
79 changes: 79 additions & 0 deletions src/ValidatorZkEmailRecovery.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import { ZkEmailRecovery } from "./ZkEmailRecovery.sol";
import { IERC7579Account } from "erc7579/interfaces/IERC7579Account.sol";

contract ValidatorZkEmailRecovery is ZkEmailRecovery {
error InvalidOldOwner();

constructor(
address _verifier,
address _dkimRegistry,
address _emailAuthImpl
)
ZkEmailRecovery(_verifier, _dkimRegistry, _emailAuthImpl)
{ }

/**
* @notice Returns a two-dimensional array of strings representing the subject templates for
* email recovery.
* @dev This function is overridden from ZkEmailRecovery. It is
* re-implemented by this contract to support a different subject template for recovering Safe
* accounts.
* in the subject or if the email should be in a language that is not English.
* @return string[][] A two-dimensional array of strings, where each inner array represents a
* set of fixed strings and matchers for a subject template.
*/
function recoverySubjectTemplates() public pure override returns (string[][] memory) {
string[][] memory templates = new string[][](1);
templates[0] = new string[](9);
templates[0][0] = "Recover";
templates[0][1] = "account";
templates[0][2] = "{ethAddr}";
templates[0][3] = "with";
templates[0][4] = "validator";
templates[0][5] = "{ethAddr}";
templates[0][6] = "using";
templates[0][7] = "calldata";
templates[0][8] = "{string}";
return templates;
}

/**
* @notice Validates the recovery subject templates and extracts the account address
* @dev This function is overridden from ZkEmailRecovery. It is re-implemented by
* this contract to support a different subject template for recovering Safe accounts.
* This function reverts if the subject parameters are invalid. The function
* should extract and return the account address as that is required by
* the core recovery logic.
* @param templateIdx The index of the template used for the recovery request
* @param subjectParams An array of bytes containing the subject parameters
* @return accountInEmail The extracted account address from the subject parameters
*/
function validateRecoverySubjectTemplates(
uint256 templateIdx,
bytes[] memory subjectParams
)
internal
view
override
returns (address)
{
if (templateIdx != 0) {
revert InvalidTemplateIndex();
}

if (subjectParams.length != 3) {
revert InvalidSubjectParams();
}

address accountInEmail = abi.decode(subjectParams[0], (address));
address validatorInEmail = abi.decode(subjectParams[1], (address));
bytes memory callData = bytes(abi.decode(subjectParams[2], (string)));

// TODO: validate

return accountInEmail;
}
}
71 changes: 71 additions & 0 deletions src/libraries/BytesLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

library BytesLib {
function slice(
bytes memory _bytes,
uint256 _start,
uint256 _length
)
internal
pure
returns (bytes memory)
{
require(_length + 31 >= _length, "slice_overflow");
require(_bytes.length >= _start + _length, "slice_outOfBounds");

bytes memory tempBytes;

assembly {
switch iszero(_length)
case 0 {
// Get a location of some free memory and store it in tempBytes as
// Solidity does for memory variables.
tempBytes := mload(0x40)

// The first word of the slice result is potentially a partial
// word read from the original array. To read it, we calculate
// the length of that partial word and start copying that many
// bytes into the array. The first word we copy will start with
// data we don't care about, but the last `lengthmod` bytes will
// land at the beginning of the contents of the new array. When
// we're done copying, we overwrite the full first word with
// the actual length of the slice.
let lengthmod := and(_length, 31)

// The multiplication in the next line is necessary
// because when slicing multiples of 32 bytes (lengthmod == 0)
// the following copy loop was copying the origin's length
// and then ending prematurely not copying everything it should.
let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod)))
let end := add(mc, _length)

for {
// The multiplication in the next line has the same exact purpose
// as the one above.
let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start)
} lt(mc, end) {
mc := add(mc, 0x20)
cc := add(cc, 0x20)
} { mstore(mc, mload(cc)) }

mstore(tempBytes, _length)

//update free-memory pointer
//allocating the array padded to 32 bytes like the compiler does now
mstore(0x40, and(add(mc, 31), not(31)))
}
//if we want a zero-length slice let's just return a zero-length array
default {
tempBytes := mload(0x40)
//zero out the 32 bytes slice we are about to return
//we need to do it because Solidity does not garbage collect
mstore(tempBytes, 0)

mstore(0x40, add(tempBytes, 0x20))
}
}

return tempBytes;
}
}
177 changes: 177 additions & 0 deletions src/modules/ValidatorRecoveryModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import { ERC7579ExecutorBase } from "@rhinestone/modulekit/src/Modules.sol";
import { IERC7579Account } from "erc7579/interfaces/IERC7579Account.sol";
import { IERC7579Module } from "erc7579/interfaces/IERC7579Module.sol";
import { ExecutionLib } from "erc7579/lib/ExecutionLib.sol";
import { ModeLib } from "erc7579/lib/ModeLib.sol";

import { IRecoveryModule } from "../interfaces/IRecoveryModule.sol";
import { IZkEmailRecovery } from "../interfaces/IZkEmailRecovery.sol";
import { ISafe } from "../interfaces/ISafe.sol";
import { BytesLib } from "../libraries/BytesLib.sol";

contract ValidatorRecoveryModule is ERC7579ExecutorBase, IRecoveryModule {
using BytesLib for bytes;
/*//////////////////////////////////////////////////////////////////////////
CONSTANTS
//////////////////////////////////////////////////////////////////////////*/

address public immutable zkEmailRecovery;

event NewValidatorRecovery(address indexed validatorModule, bytes4 recoverySelector);

error NotTrustedRecoveryContract();
error InvalidSubjectParams();
error InvalidValidator(address validator);
error InvalidSelector(bytes4 selector);

mapping(address validatorModule => mapping(address account => bytes4 allowedSelector)) internal
$allowedSelector;

constructor(address _zkEmailRecovery) {
zkEmailRecovery = _zkEmailRecovery;
}

modifier onlyZkEmailRecovery() {
if (msg.sender != zkEmailRecovery) revert NotTrustedRecoveryContract();

_;
}

modifier withoutUnsafeSelector(bytes4 recoverySelector) {
if (
recoverySelector == IERC7579Module.onUninstall.selector
|| IERC7579Module.onInstall.selector
) {
revert InvalidValidator(recoverySelector);
}

_;
}

/*//////////////////////////////////////////////////////////////////////////
CONFIG
//////////////////////////////////////////////////////////////////////////*/

function allowValidatorRecovery(
address validator,
bytes calldata isInstalledContext,
bytes4 recoverySelector
)
public
withoutUnsafeSelector(recoverySelector)
{
if (
!IERC7579Account(account).isModuleInstalled(
TYPE_VALIDATOR, validator, isInstalledContext
)
) {
revert InvalidValidator(validatorModule);
}
$allowedSelector[validator][msg.sender] = recoverySelector;

emit NewValidatorRecovery({ validatorModule: validator, recoverySelector: recoverySelector });
}

/**
* Initialize the module with the given data
* @param data The data to initialize the module with
*/
function onInstall(bytes calldata data) external {
(
address[] memory guardians,
uint256[] memory weights,
uint256 threshold,
uint256 delay,
uint256 expiry
) = abi.decode(data, (address[], uint256[], uint256, uint256, uint256));

// TODO: add initialization with allowValidatorRecovery()
_execute({
to: zkEmailRecovery,
value: 0,
data: abi.encodeCall(
IZkEmailRecovery.configureRecovery,
(address(this), guardians, weights, threshold, delay, expiry)
)
});
}

/**
* De-initialize the module with the given data
* @custom:unusedparam data - the data to de-initialize the module with
*/
function onUninstall(bytes calldata /* data */ ) external {
IZkEmailRecovery(zkEmailRecovery).deInitRecoveryFromModule(msg.sender);
}

/**
* Check if the module is initialized
* @param smartAccount The smart account to check
* @return true if the module is initialized, false otherwise
*/
function isInitialized(address smartAccount) external view returns (bool) {
return IZkEmailRecovery(zkEmailRecovery).getGuardianConfig(smartAccount).threshold != 0;
}

/*//////////////////////////////////////////////////////////////////////////
MODULE LOGIC
//////////////////////////////////////////////////////////////////////////*/

function recover(
address account,
bytes[] calldata subjectParams
)
external
onlyZkEmailRecovery
{
// prevent out of bounds error message, in case subject params are invalid
if (subjectParams.length < 3) {
revert InvalidSubjectParams();
}

address validatorModule = abi.decode(subjectParams[1], (address));
bytes memory recoveryCallData = bytes(abi.decode(subjectParams[2], (string)));

bytes4 selector = bytes4(recoveryCallData.slice({ _start: 0, _length: 4 }));
if ($allowedSelector[validatorModule][account] != selector) {
revert InvalidSelector(selector);
}
_execute({ account: account, to: validatorModule, value: 0, data: recoveryCallData });
}

function getTrustedContract() external view returns (address) {
return zkEmailRecovery;
}

/*//////////////////////////////////////////////////////////////////////////
METADATA
//////////////////////////////////////////////////////////////////////////*/

/**
* The name of the module
* @return name The name of the module
*/
function name() external pure returns (string memory) {
return "ValidatorRecoveryModule";
}

/**
* The version of the module
* @return version The version of the module
*/
function version() external pure returns (string memory) {
return "0.0.1";
}

/**
* Check if the module is of a certain type
* @param typeID The type ID to check
* @return true if the module is of the given type, false otherwise
*/
function isModuleType(uint256 typeID) external pure returns (bool) {
return typeID == TYPE_EXECUTOR;
}
}