Skip to content

Commit

Permalink
Update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnGuilding committed Jun 3, 2024
1 parent b16ebd4 commit 164dc76
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 26 deletions.
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ cache/
out/
broadcast

# Docs
docs/

# Dotenv file
.env
node_modules
110 changes: 90 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
## ZK Email Recovery 7579 Plugin
## ZK Email Recovery

## Overview
Account recovery has traditionally been one of the most complex UX hurdles that account holders have to contend with. The ZK Email Recovery contracts provide a robust and simple mechanism for account holders to recover accounts via email guardians.

## Usage

Expand All @@ -21,12 +24,16 @@ forge test
```

### invalid subject error
If you experience certain tests failing with the error `revert: invalid subject`. This means that there has been an update to one of the files that has resulted in the subject changing for the tests. The tests use hardcoded subjects, and when an address changes in the subject, these hardcoded values need to be updated. This is normally the recovery module address that needs updating when you change a core file like `ZkEmailRecovery.sol`. There is a known issue with CI being investiagted where a number of tests are failing because of this issue but local test runs pass successfully. If your local tests fail, you need to go to the base test files and uncomment the relevant console logs:
If you experience certain tests failing with the error `revert: invalid subject`. This means that there has been an update to one of the files that has resulted in the subject changing for the tests. The tests use hardcoded subjects, and when an address changes in the subject, these hardcoded values need to be updated. This is normally the recovery module address that needs updating when you change a core file like `ZkEmailRecovery.sol`. _There is a known issue with CI being investiagted where a number of tests are failing because of this issue but local test runs pass successfully._

If your local tests fail, you need to go to the base test files and uncomment the relevant console logs:
- If the test failing is located in test/integration/OwnableValidatorRecovery, then go to test/integration/OwnableValidatorRecovery/OwnableValidatorBase.t.sol
- If the test failing is located in test/integration/SafeRecoveryModule, then go to test/integration/SafeRecovery/SafeIntegrationBase.t.sol
- If the test failing is located in test/unit/, then go to test/unit/UnitBase.t.sol

Uncomment lines such as `console2.log("recoveryModule: ", recoveryModule);`, and then compare the hardcoded subjects to values printed in the console. If you uncomment the recovery module console log and see the following:
Uncomment lines such as `console2.log("recoveryModule: ", recoveryModule);`, and then compare the hardcoded subjects to values printed in the console.

If you uncomment some console logs for the process recovery subject generation and see the following:
```bash
accountAddress: 0x50Bc6f1F08ff752F7F5d687F35a0fA25Ab20EF52
newOwner: 0x7240b687730BE024bcfD084621f794C2e4F8408f
Expand All @@ -38,44 +45,93 @@ But the hardcoded subject is:
Recover account 0x50Bc6f1F08ff752F7F5d687F35a0fA25Ab20EF52 to new owner 0x7240b687730BE024bcfD084621f794C2e4F8408f using recovery module 0x0957519DC28b1d289F4721244416C3F00033747c
```

You can see that the account address and new owner address are the same in the console and the same in the hardcoded value. As it is the recovery module address that is different, you will need to update the final address in the subjecy to the new recovery module address printed in the console.
You can see that the account address and new owner address are the same in the console and the same in the hardcoded value. As it is the recovery module address that is different, you will need to update the final address in the subject to the new recovery module address printed in the console.

# ZK Email Recovery

## High level contracts diagram.
TODO:
## High level contracts diagram (WIP).
<img src="docs/recovery-module-contracts.drawio.png">

## Core contracts
The core contracts contain the bulk of the recovery logic.

### ZkEmailRecovery.sol

`ZkEmailRecovery.sol` defines a default implementation for email-based recovery. It is designed to provide the core logic for email based account recovery that can be used across different account implementations. For the end user, the core `ZkEMailRecovery` contract aims to provide a robust and simple mechanism to recover accounts via email guardians.
`ZkEmailRecovery.sol` defines a default implementation for email-based recovery. It is designed to provide the core logic for email based account recovery that can be used across different modular account implementations. For the end user, the core `ZkEMailRecovery` contract aims to provide a robust and simple mechanism to recover accounts via email guardians.

It inherits from a zk email contract called `EmailAccountRecovery.sol` which defines some basic recovery logic that interacts with lower level zk email contracts. `EmailAccountRecovery.sol` holds the logic that interacts with the lower level zk email contracts EmailAuth.sol, verifier, dkim registry etc. More info on the underlying EmailAccountRecovery.sol contract: https://github.com/zkemail/ether-email-auth/tree/main/packages/contracts#emailaccountrecovery-contract.

The guardians are represented onchain by EmailAuth.sol instances. EmailAuth.sol is a lower level zk email contract, it is designed to be generic, and allows dapps to authorize anything described in an email. The guardians privacy is protected onchain, for more info on zk email privacy and EmailAuth - see the [zk email docs](https://zkemail.gitbook.io/zk-email).
The guardians are represented onchain by EmailAuth.sol instances. EmailAuth.sol is a lower level zk email contract. it is designed to authenticate that a user is a correct holder of the specific email address and authorize anything described in the email. The guardians privacy is protected onchain, for more info on zk email privacy and EmailAuth - see the [zk email docs](https://zkemail.gitbook.io/zk-email).

ZkEmailRecovery relies on a dedicated recovery module to execute a recovery attempt. This (ZkEmailRecovery) contract defines "what a valid recovery attempt is for an account", and the recovery module defines “how that recovery attempt is executed on the account”. One motivation for having the 7579 recovery module and the core ZkEMailRecovery contract being seperated is to allow the core recovery logic to be used across different account implementations and module standards. The core `ZkEmailRecovery.sol` contract is designed to be account implementation agnostic and can be extended for a wide range of modular account implementations.

ZkEmailRecovery relies on a dedicated recovery module to execute a recovery attempt. This (ZkEmailRecovery) contract defines "what a valid recovery attempt is for an account", and the recovery module defines “how that recovery attempt is executed on the account”. One motivation for having the 7579 recovery module and the core ZkEMailRecovery contract being seperated is to allow the core recovery logic to be used across different account implementations and module standards. The core `ZkEmailRecovery.sol` contract is designed to be account implementation agnostic and can be extended for a wide range of account implementations.
## ZkEmailRecovery flow walkthrough

The core functions that must be called in the end-to-end flow for recovery are
1. configureRecovery (does not need to be called again for subsequent recovery attempts)
2. handleAcceptance - called for each guardian. Defined on EmailAccountRecovery.sol, calls acceptGuardian in this contract
3. handleRecovery - called for each guardian. Defined on EmailAccountRecovery.sol, calls processRecovery in this contract
4. completeRecovery

Importantly this contract offers the functonality to recover an account via email in a scenario where a private key has been lost. This contract does NOT provide an adequate mechanism to protect an account from a stolen private key by a malicious actor. This attack vector requires a holistic approach to security that takes specific implementation details of an account into consideration. For example, adding additional access control when cancelling recovery to prevent a malicious actor stopping recovery attempts, and adding spending limits to prevent account draining. This contract is designed to be extended to take these additional considerations into account, but does not provide them by default.
Before proceeding, ensure that the following components are correctly set up before deploying a ZkEmailRecovery instance:
- verifierAddr: Address of the ZKP verifier.
- dkimAddr: Address of the DKIM registry for email authentication.
- emailAuthImplementationAddr: Address of the email authentication implementation.

### EmailAccountRecoveryRouter.sol
`EmailAccountRecoveryRouter.sol` is a helper contract that routes relayer calls to the correct `EmailAccountRecovery` implementation. Originally, the abstract `EmailAccountRecovery.sol` contract was designed to be implemented on the acount contract itself (as opposed to a module), and so `completeRecovery()` did not have any context for which account to call, as it was assumed you were already calling the account - so no arguments needed. For account types such as a modular accounts that use modules/plugins that often share state among many accounts, you do need some context for which account you want to recover when calling completeRecovery. The rationale for the router is to provide a way to provide context for which account completeRecovery should target - **the key line of code that's used to do this is getAccountForRouter(msg.sender) in ZkEmailRecovery.sol**.
### Configure Recovery
After deployment, this is the first core function in the recovery flow, setting up the recovery module, guardians, guardian weights, threshold, and delay/expiry. It only needs to be called once. The threshold must be set appropriately to balance security and usability. The same goes for the delay and expiry - there is a minimum recovery window time that protects against an account giving itself a prohibitively small window in which to complete a recovery attempt.

This represents a slight workaround as EmailAccountRecovery was already audited and the completeRecovery interface could not be changed - so the router could be removed in future iterations.
It is strongly recommended that configureRecovery is called during the installation phase of the recovery module. This ensures that a user cannot forget to install the recovery module and end up with a broken recovery config.

### SafeZkEmailRecovery.sol
`SafeZkEmailRecovery.sol` is an extension of `ZkEmailRecovery.sol` that implements recovery for Safe accounts. It provides a good example of how to extend `ZkEmailRecovery.sol` for different account implementations.
```ts
function configureRecovery(
address recoveryModule,
address[] memory guardians,
uint256[] memory weights,
uint256 threshold,
uint256 delay,
uint256 expiry
) external;
```

### Handle Acceptance
This function handles the acceptance of each guardian. Each guardian must accept their role to be a part of the recovery process. This is an important step as it ensures that the guardian consents to the responsibility of being a guardian for a specific account, and protects against typos from entering a guardian email address. Such a typo would render the guardian unusable. handleAcceptance must be called for each guardian until the threshold is reached.

```ts
function handleAcceptance(
address guardian,
uint256 templateIdx,
bytes[] memory subjectParams,
bytes32
) internal;
```

### How you can extend ZkEMailRecovery by adding a custom template
### Handle Recovery
This function processes each guardian's recovery request. A Guardian can initiate a recovery request by replying to an email. The contract verifies the guardian's status and checks if the threshold is met. Once the threshold is met and the delay has passed, anyone can complete the recovery process. The recovery delay is a security feature that gives the wallet owner time to react to a recovery attempt in case of a malicious guardian or guardians. This is possible from guardians who act maliciously, but also from attackers who have gotten access to a guardians email address. Although since guardian email privacy is preserved on chain, this reduces the attack surface further since someone with access to a someone elses email account would not know if the email address is used in a recovery setup, or if they did, which account to target. There is also an expiry time, which once expires, invalidates the recovery attempt. This encourages timely execution of recovery attempts and reduces the attack surface that could result from recovery attempts that have been stagnent and uncompleted for long periods of time.

```ts
function handleRecovery(
address guardian,
uint256 templateIdx,
bytes[] memory subjectParams,
bytes32
) internal;
```

### Complete Recovery
The final function to complete the recovery process. This function completes the recovery process by validating the recovery request and triggering the recovery module to perform the recovery on the account itself.

```ts
function completeRecovery(address account) public;
```

### EmailAccountRecoveryRouter.sol
`EmailAccountRecoveryRouter.sol` is a helper contract that routes relayer calls to the correct `EmailAccountRecovery` implementation. Originally, the abstract `EmailAccountRecovery.sol` contract was designed to be implemented on the acount contract itself (as opposed to a module, or an external contract that supports recovering multiple accounts), and so `completeRecovery()` did not have any context for which account to call, as it was assumed you were already calling the account - so no arguments where needed in the call. For account types such as a modular accounts that use modules/plugins that often share state among many accounts, you do need some context for which account you want to recover when calling completeRecovery. The rationale for the router is to provide a way to provide context for which account `completeRecovery` should target - **the key line of code that's used to do this is `address account = getAccountForRouter(msg.sender);` in `completeRecovery()` ZkEmailRecovery.sol**. This represents a slight workaround as EmailAccountRecovery was already audited and the completeRecovery interface could not be changed - so the router could be removed in future iterations.

### SafeZkEmailRecovery.sol
`SafeZkEmailRecovery.sol` is an extension of `ZkEmailRecovery.sol` that implements recovery for Safe accounts. It provides a good example of how to extend `ZkEmailRecovery.sol` for different account implementations. The contract follows the same multi-step process that ZkEmailRecovery.sol does, where guardians must first be accepted before they can initiate recovery. The Safe example does not override the acceptance related functions, only the recovery ones. This is because the acceptance subject is broad enough that it can stay the same whereas the recovery attempt subject needs additional info in order to complete the recovery request properly. This will be a common scenario where only the recovery subject-related functions will need overriding. A scenario in which you would definitely need to update both is if you wanted to provide email recovery functionality to users who didn't speak English. In which case you could translate the required subjects into the chosen language

## How you can extend ZkEmailRecovery by adding a custom template

When you know what recovery specific information you need, you can create a new contract, inherit from `ZkEmailRecovery.sol` and extend the relevant functions - note you only have to extend ones that are relevant:
* `acceptanceSubjectTemplates()`
Expand All @@ -98,11 +154,22 @@ The subject params would be:
```ts
bytes[] memory subjectParamsForRecovery = new bytes[](3);
subjectParamsForRecovery[0] = abi.encode(accountAddress);
subjectParamsForRecovery[1] = abi.encode(oldOwner);
subjectParamsForRecovery[2] = abi.encode(newOwner);
subjectParamsForRecovery[3] = abi.encode(recoveryModule);
subjectParamsForRecovery[1] = abi.encode(newOwner);
subjectParamsForRecovery[2] = abi.encode(recoveryModule);
```

### What can I add to a subject template?
A subject template defines the expected format of the message in the Subject for each recovery implementation. It allows developers to constrain that message to be in the application-specific format without writing new ZKP circuits. The use of different subject templates in this case allows for a flexible and extensible mechanism to define recovery messages, making it adaptable to different modular account implemtations.

The subject template is an array of strings, each of which has some fixed strings without space and the following variable parts:
- `"{string}"`: a string. Its Solidity type is `string`.
- `"{uint}"`: a decimal string of the unsigned integer. Its Solidity type is `uint256`.
- `"{int}"`: a decimal string of the signed integer. Its Solidity type is `int256`.
- `"{decimals}"`: a decimal string of the decimals. Its Solidity type is `uint256`. Its decimal size is fixed to 18. E.g., “2.7” ⇒ `abi.encode(2.7 * (10**18))`.
- `"{ethAddr}"`: a hex string of the Ethereum address. Its Solidity type is `address`. Its value MUST satisfy the checksum of the Ethereum address.

If you are recovering an account that needs to rotate a public key which is of type `bytes` in solidity, you can use the string type for that for the subject template.

## 7579 Modules
The 7579 recovery modules are implementation specific and are the main contracts that define how a specific account is recovered. They are meant to make to be relatively simple for a module developer to build email recovery into an account.

Expand All @@ -114,4 +181,7 @@ The `recover()` function on the module holds the core logic for the module. It d
An example recovery module that recovers a 7579 OwnableValidator

### SafeRecoveryModule.sol
An example recovery module that recovers a Safe account
An example recovery module that recovers a Safe account

## Threat model
Importantly this contract offers the functonality to recover an account via email in a scenario where a private key has been lost. This contract does NOT provide an adequate mechanism to protect an account from a stolen private key by a malicious actor. This attack vector requires a holistic approach to security that takes specific implementation details of an account into consideration. For example, adding additional access control when cancelling recovery to prevent a malicious actor stopping recovery attempts, and adding spending limits to prevent account draining. This contract is designed to be extended to take these additional considerations into account, but does not provide them by default.
Binary file added docs/recovery-module-contracts.drawio.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 164dc76

Please sign in to comment.