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 | bash
git clone https://github.com/zkemail/ether-email-auth.git
Move to the packages/contracts
directory and run the following command.
yarn install
Also, build the contract by running the following command.
yarn build
First, 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 .env
Then, set the following environment variables to .env
PRIVATE_KEY= # Your private key with 0x prefix
ETHERSCAN_API_KEY= # Your Basescan API key
After 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 -vvvv
That's all for the contracts side.
Developers can use the relayer and prover we prepared for you. Refer to the following API endpoint to send a request.