From cd0c0d47e33b29e5fdd0f681e61647308f7bc354 Mon Sep 17 00:00:00 2001 From: zeroknots Date: Fri, 31 May 2024 10:37:03 +0700 Subject: [PATCH] feat: adding validator recovery flow wip --- src/ValidatorZkEmailRecovery.sol | 79 +++++++++++ src/libraries/BytesLib.sol | 71 ++++++++++ src/modules/ValidatorRecoveryModule.sol | 177 ++++++++++++++++++++++++ 3 files changed, 327 insertions(+) create mode 100644 src/ValidatorZkEmailRecovery.sol create mode 100644 src/libraries/BytesLib.sol create mode 100644 src/modules/ValidatorRecoveryModule.sol diff --git a/src/ValidatorZkEmailRecovery.sol b/src/ValidatorZkEmailRecovery.sol new file mode 100644 index 00000000..1dbb95f9 --- /dev/null +++ b/src/ValidatorZkEmailRecovery.sol @@ -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; + } +} diff --git a/src/libraries/BytesLib.sol b/src/libraries/BytesLib.sol new file mode 100644 index 00000000..1cd66b10 --- /dev/null +++ b/src/libraries/BytesLib.sol @@ -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; + } +} diff --git a/src/modules/ValidatorRecoveryModule.sol b/src/modules/ValidatorRecoveryModule.sol new file mode 100644 index 00000000..042674f6 --- /dev/null +++ b/src/modules/ValidatorRecoveryModule.sol @@ -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; + } +}