The following steps are for developers to implement account recovery using ether-email-auth.
First, install foundry by running the following command:
curl -L https://foundry.paradigm.xyz | bashgit clone https://github.com/zkemail/ether-email-auth.gitMove to the packages/contracts directory and run the following command.
yarn installAlso, build the contract by running the following command.
yarn buildFirst, implement a simple wallet. Use the following implementation of SimpleWallet. (SimpleWallet.sol)[../packages/contracts/contracts/SimpleWallet.sol] This implementation inherits OwnableUpgradeable.
This function is implemented to change the owner of this wallet.
function changeOwner(address newOwner) public {
require(
msg.sender == owner() || msg.sender == recoveryController,
"only owner or recovery controller"
);
_transferOwnership(newOwner);
}Implement a RecoveryController to execute EmailAuth. Implement the following implementation of RecoveryController. (RecoveryController.sol)[../packages/contracts/contracts/RecoveryController.sol]
The Controller account must inherit EmailAccountRecovery.sol.
contract RecoveryController is OwnableUpgradeable, EmailAccountRecovery {Implement the status of the Guardian to execute Account Recovery.
enum GuardianStatus {
NONE,
REQUESTED,
ACCEPTED
}Implement the following mapping.
mapping(address => bool) public isRecovering; // Whether the account address is being recovered
mapping(address => address) public newSignerCandidateOfAccount; // The new signer candidate of the account address
mapping(address => GuardianStatus) public guardians; // The status of the guardian of the account address
mapping(address => uint) public timelockPeriodOfAccount; // The timelock period of the account address
mapping(address => uint) public currentTimelockOfAccount; // The current timelock of the account address
Define the subject of the email when the guardian requests.
function acceptanceSubjectTemplates()
public
pure
override
returns (string[][] memory)
{
string[][] memory templates = new string[][](1);
templates[0] = new string[](5);
templates[0][0] = "Accept";
templates[0][1] = "guardian";
templates[0][2] = "request";
templates[0][3] = "for";
templates[0][4] = "{ethAddr}";
return templates;
}Define the subject of the email when the recovery is executed.
function recoverySubjectTemplates()
public
pure
override
returns (string[][] memory)
{
string[][] memory templates = new string[][](1);
templates[0] = new string[](8);
templates[0][0] = "Set";
templates[0][1] = "the";
templates[0][2] = "new";
templates[0][3] = "signer";
templates[0][4] = "of";
templates[0][5] = "{ethAddr}";
templates[0][6] = "to";
templates[0][7] = "{ethAddr}";
return templates;
}Implement a method to return the account address to be recovered from AcceptanceSubject.
The account address to be recovered is stored in templates[0][4] in the implementation of acceptanceSubjectTemplates.
This is the first element of subjectParams, so return subjectParams[0].
function extractRecoveredAccountFromAcceptanceSubject(
bytes[] memory subjectParams,
uint templateIdx
) public pure override returns (address) {
require(templateIdx == 0, "invalid template index");
require(subjectParams.length == 1, "invalid subject params");
return abi.decode(subjectParams[0], (address));
}Implement a method to return the account address to be recovered from RecoverySubject.
The account address to be recovered is stored in templates[0][6] in the implementation of recoverySubjectTemplates.
This is the first element of subjectParams, so return subjectParams[0].
function extractRecoveredAccountFromRecoverySubject(
bytes[] memory subjectParams,
uint templateIdx
) public pure override returns (address) {
require(templateIdx == 0, "invalid template index");
require(subjectParams.length == 2, "invalid subject params");
return abi.decode(subjectParams[0], (address));
}Implement a method to accept the guardian.
If AcceptanceSubject is used, the account address to be recovered is stored in subjectParams[0].
This address must not be in isRecovering.
Next, check if the guardian is in the REQUESTED status.
Finally, change the status of the guardian to ACCEPTED.
function acceptGuardian(
address guardian,
uint templateIdx,
bytes[] memory subjectParams,
bytes32
) internal override {
address account = abi.decode(subjectParams[0], (address));
require(!isRecovering[account], "recovery in progress");
require(guardian != address(0), "invalid guardian");
require(
guardians[guardian] == GuardianStatus.REQUESTED,
"guardian status must be REQUESTED"
);
require(templateIdx == 0, "invalid template index");
require(subjectParams.length == 1, "invalid subject params");
guardians[guardian] = GuardianStatus.ACCEPTED;
}Implement a method to execute recovery.
If RecoverySubject is used, the new signer is stored in subjectParams[1].
subjectParams[0] is the account address to be recovered.
Check if this address is not in isRecovering.
Next, check if the guardian is in the ACCEPTED status.
Finally, set isRecovering to true and update newSignerCandidateOfAccount and currentTimelockOfAccount.
function processRecovery(
address guardian,
uint templateIdx,
bytes[] memory subjectParams,
bytes32
) internal override {
address account = abi.decode(subjectParams[0], (address));
require(!isRecovering[account], "recovery in progress");
require(guardian != address(0), "invalid guardian");
require(
guardians[guardian] == GuardianStatus.ACCEPTED,
"guardian status must be ACCEPTED"
);
require(templateIdx == 0, "invalid template index");
require(subjectParams.length == 2, "invalid subject params");
address newSignerInEmail = abi.decode(subjectParams[1], (address));
require(newSignerInEmail != address(0), "invalid new signer");
isRecovering[account] = true;
newSignerCandidateOfAccount[account] = newSignerInEmail;
currentTimelockOfAccount[account] =
block.timestamp +
timelockPeriodOfAccount[account];
}Implement a method to complete recovery.
Check if this address is being recovered.
Next, check if the timelock is not expired.
Finally, set isRecovering to false and update newSignerCandidateOfAccount and currentTimelockOfAccount.
Then, call SimpleWallet.changeOwner to change the owner to the new signer.
function completeRecovery(
address account,
bytes memory recoveryCalldata
) public override {
require(account != address(0), "invalid account");
require(isRecovering[account], "recovery not in progress");
require(
currentTimelockOfAccount[account] <= block.timestamp,
"timelock not expired"
);
address newSigner = newSignerCandidateOfAccount[account];
isRecovering[account] = false;
currentTimelockOfAccount[account] = 0;
newSignerCandidateOfAccount[account] = address(0);
SimpleWallet(payable(account)).changeOwner(newSigner);
}First, set the environment variables.
You should set the following environment variables to .env
Your PRIVATE_KEY needs some gas fees to deploy.
cp .env.example .envThen, set the following environment variables to .env
PRIVATE_KEY= # Your private key with 0x prefix
ETHERSCAN_API_KEY= # Your Basescan API keyAfter that, deploy the contract by running the following command.
source .env
forge script script/DeployRecoveryController.s.sol:Deploy --rpc-url $SEPOLIA_RPC_URL --chain-id $CHAIN_ID --etherscan-api-key $ETHERSCAN_API_KEY --broadcast --verify -vvvvThat's all for the contracts side.
Developers can use the relayer we prepared for you. Refer to the following API endpoint to send a request.