Building Email Wallet

11/30/2023 | 1 hr read

How we created a wallet to send any asset via email.

Special thanks to Saleel for writing the post, and Aayush and Sora for the review and suggestions.

Introduction

This is a technical introduction to the Email Wallet project, explaining what it does and how it works internally.

Email Wallet is a smart contract wallet that can be operated using emails. Essentially, one can interact with the Ethereum blockchain by simply sending emails.

Email Wallet is built on top of ZK Email. ZK Email uses ZK Snarks to prove possession of an email and can selectively disclose information contained in the email.

Credits: ZK Email was originally created by Aayush, Sora, and Sampriti. Email Wallet was introduced (and much of the spec below was created) by Sora. Sora, Aayush, myself, Rasul, Wataru, Elo worked on the development of Email Wallet. Please check ZK Email Org for more details.

ZK Email

Here is a quick overview of how ZK Email works; for more details, please refer to the Aayush's blog on the same:

  • Emails are (almost always) signed by the sender's email provider using a protocol called DKIM Signatures.
  • The From Address, Subject and Body (hashed) of the email are usually always signed. The details of the signed fields, the algorithm used, and the signature itself are included in the DKIM-Signature header of the email.
  • rsa-sha256 is the most common signature algorithm used by email providers.
  • The public key used for signing is published as a DNS record for the sender's domain. The selector needed to query the right DNS record is part of the DKIM-Signature header.
  • ZK Email uses a ZK circuit to verify the email signature using the DKIM public key and prove the necessary properties of the email without exposing the whole email.
  • Information needed to be disclosed can be added as public input of the circuit.
  • ZK-Regex is used to extract and prove specific information from email content using regular expressions.
  • In short, you can prove you have an email "sent from an email address", "contains a particular subject", or "have a specific word in the body".
  • Smart contracts can verify the proof on-chain by validating that the DKIM public key used when generating the proof is the same as the one stored in the on-chain DKIM Registry for the domain.

Email Wallet

Email Wallet uses proof of email from a user to operate the user's Ethereum account (contract wallet). Basically, the DKIM signature acts as the signature for the user's Ethereum account (instead of a private key held in Metamask for example).

DKIM signatures can be directly verified on-chain, but this would reveal the entire email content, and users won't have any privacy. This is why using ZK Email is important - we can create proof of necessary information from email content without revealing the user's or recipient's email address.

How it works

A new account contract is deployed for each user, which holds the user's funds. The owner of this contract can execute any calldata on any target contract on behalf of the account. See Wallet.sol

The owner of the account contract is the EmailWalletCore contract by default. Core contract validates the EmailOperation and executes the intended "operation" on the Wallet contract. See EmailWalletCore.sol

Basically, the flow works like this:

  • Users send an email to a "Relayer" server with their intent in the email subject. For example, Send 10 DAI to friend@gmail.com
  • The relayer creates the ZK proof of the mail and calls the Core contract (handleEmailOp) with proof of email and parameters extracted from the subject.
  • The Core contract validates the proof, ensures that extracted parameters match the actual signed subject, and executes the operation on the account contract.

Features

Below are some things you could do with Email Wallet and corresponding examples of email subjects the user should send:

  • Send ETH to an email address and Ethereum addresses.

    • Send 1 ETH to friend@domain.com
    • Send 2.5 ETH to 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
  • Send ERC20 to an email address and Ethereum addresses.

    • Send 1.5 DAI to friend@skiff.com
    • Send 21.14 DAI to 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
  • Execute any calldata on a target contract.

    • Execute 0xba7676a8.....

Apart from the simple token transfer operations, developers can build extensions to interact with other contracts. For example, we have built an NFT extension for ERC721 token transfers and a Uniswap extension for swapping tokens.

  • Once extensions are published, users can install them with subjects like:

    • Install extension Uniswap
    • and use the extensions with subjects like Swap 1 ETH to DAI
    • Here, the Swap "command" is defined by the Uniswap extension when it is published (along with the name and extension contract address).
  • The user can also exit Email Wallet by sending an email like:

    • Exit Email Wallet. Set owner to 0xf66f...
    • The owner (EOA) can do any operation on the account now by calling the execute method.


There were many problems and technical challenges we faced when building Email Wallet, mainly to protect user privacy and prevent Relayer from being malicious. Below are some of them:

Email Address Privacy

For privacy reasons, we do not want to reveal the email address of the users (nor the hash of the email address) on-chain.

So the address of the account contract is derived from an AccountKey which is a randomly generated value. Specifically, the CREATE2 salt of the user's account contract is hash(accountKey, 0).

Relayer maintains the mapping between an email address and the account key.

Users create an email wallet by sending an email to Relayer with a subject like Create Account with CODE:0xababab11 where the last part after CODE: is the Account Key.

Relayer registers a new account for a user with a commitment like hash(emailAddress, accountKey). Relayer uses this commitment to prove that the email came from the same user later, when the user sends an email with an operation. i.e. this commitment is an output of the circuit that generates proof of email from the user for an operation. See AccountKeyCommitment below.

Related circuits: EmailSender

Account nullifier

The Relayer creates an account for the user in the Core contract which deploys an account contract for the user. Relayer needs to produce a proof of email from the user with AccountKey containing anywhere in email headers.

To prevent Relayer from creating multiple accounts for the same email address, Relayer needs to commit to the user's email address and the account key.

Relayer maintains a secret value relayerSecret and commits hash(relayerSecret) on-chain when registering as a Relayer. Relayer then provides EmailPointer (hash(email, relayerSecret)), AccountKeyCommitment (hash(email, accountKey, hash(relayerSecret))) and proof they are generated correctly when registering an account.

Core contract ensures EmailPointer and AccountKeyCommitment are unique.

  • AccountKeyCommitment ensures that the user has explicitly sent an email to Relayer with Account Key before the relayer can execute EmailOps for the user. This will prevent emails with matching subjects sent to random people for other purposes from being used for EmailOps.
  • EmailPointer ensures Relayer can only create one account for one email address. This will prevent malicious Relayer from using random values from multiple emails as the account key. This will also prevent the user from "accidentally" creating multiple accounts for the same email address (in case the Relayer doesn't check the existence of the email address off-chain).

We can remove EmailPointer by having the circuit check for the Account Key using a specific prefix that is less likely to be found in "other" emails. The CODE prefix does this already, and EmailPointer can be removed in future versions of Email Wallet.

Related code: AccountCreation circuit, AccountInitialization circuit, and AccountHandler.sol.

Subject validation

Extracting the parameters from an email subject is difficult to do on-chain.

Instead, Relayer extracts the subject parameters off-chain and is passed in EmailOp, and Core contract constructs the subject from the EmailOp, and validates it against the signed subject (which is also passed in the EmailOp).

Note that verifying the proof of email (which happens in handleEmailOp) ensures the subject was sent by the user.

For privacy reasons, the email address (of the recipient) is masked and replaced with 0 bits when it is output from the circuit.

Related code: EmailSender circuit and SubjectUtils.sol contract

Email Nullifier

To prevent Relayer from creating multiple transactions from the same email address, we need to add a nullifier to each email proof.

Currently, the nullifier is generated in the circuit using hash(emailSignature). The core contract maintains used nullifiers, and thus ensures each email is used only once.

Related code: EmailSender circuit

Email expiration and transaction ordering

There are cases where an email from the user should be considered "outdated".

  • For example, a user sends an email to Relayer A, but their server is "down" at that moment, and the user doesn't get a response. The user sends the same email to Relayer B which executes the transaction. Relayer A comes back online later and processes the email, ending up executing the "same" transaction twice.

  • Relayer executes multiple emails from the same user in a different order, either by mistake (maybe due to race conditions when processing emails in parallel) or maliciously.

We can use the timestamp used in the DKIM signature to prevent both cases.

The core contract can prevent emails older than a limit, and a user should only email another relayer if they don't see the transaction executed by the original relayer within a limit. Timestamp can also be used as "nonce" to prevent the second case by allowing operations with only increasing timestamps.

However, not all email providers include the timestamp in the DKIM signature. While this is implemented now (first case), it needs to be removed and replaced with a solution that works for all providers.

Sending money to unregistered emails

We want users to be able to send money to an email address that doesn't already have an email wallet.

For this, we introduce something called Unclaimed Funds. When a user sends tokens to an email address, they are transferred to the core contract, and an UnclaimedFund is created for this token. UnclaimedFunds contains a commitment to the recipient's email address (hash(recipientAddr, rand)).

Once the recipient creates an account, their relayer can claim the unclaimed funds by providing proof that the recipient's AccountKeyCommitment and UnclaimedFund's EmailCommitment are from the same email address.

UnclaimedFunds have an expiration time of 30 days. So in case the recipient does not create an account within 30 days, the sender can claim the funds back (which is automatically done by relayer).

An EmailOp can have either an ETH recipient address or a commitment to the recipient's email address.

UnclaimedFunds can also be registered externally. This allows non-email-wallet users to send money to an email by registering an unclaimed fund and then sharing the EmailCommitment randomness with the recipient's (or any) Relayer.

Related code: Claim circuit, UnclaimsHandler.sol.

Extensions

As mentioned above, extensions allow emails to be used for interacting with any smart contract. You can use email-wallet-sdk to build extensions for Email Wallet.

Various "matchers" like {string}, {recipient}, {uint} are available for extension developers to define subject templates. Relayer will construct the EmailOp (and Core contract validate) by selecting a template (from installed extensions of the user) that matches the email subject.

To prevent misuse, extensions can only execute on the user's account contract when the target contract is non-ERC20. If extensions need to manage user funds, they should call a special method on the Core contract instead, which validates the requested token and amount is allowed as per the email subject.

We also have a concept of UnclaimedState similar to UnclaimedFund above, where extensions can use it to store custom "state" for email wallet users. This can be used to build an NFT extension (for example) where tokenAddr + tokenId is stored in the UnclaimedState.

Related code: UnclaimsHandler.sol and AccountHandler.sol.

Relayer Censorship

Since users need to send emails to Relayer to operate their wallet, the Relayer has the power to censor users.

To overcome this, we have a permission-less relayer network where anyone can run a relayer and users can use any relayer they want.

When the user wants to use a new relayer, they forward their original account creation email to the new relayer. Since this email contains the user's account key, the new relayer can "transport" their account using the proof of email from the user containing the account key. This way, users can use any relayer by maintaining the same wallet address.

Related code: AccountTransport circuit and RelayerHandler.sol.

Relayer Communication

As there are multiple relayers and users could be "registered" with different relayers, there is a problem when a user sends money to an email address that is registered under a different relayer.

i.e. when a user sends money to an email address, an UnclaimedFund is created for them. However, since the sender's relayer doesn't have an account for the recipient, they cannot claim the UnclaimedFund to the recipient's account.

To solve this, we have a relayer communication protocol using PSI (Private Set Intersection). Relayers commit a PSI point for each account on-chain when creating an account. Relayers communicate using an API to check if they have an account for a particular email address without revealing the email address (using PSI).

If the sender's Relayer finds another relayer who has an account for the recipient (by verifying the returned PSI point on-chain), they send the randomness used in EmailCommitment of UnclaimedFund to the recipient's relayer. The recipient's relayer can then use this to generate proof and claim the funds to the recipient's account.

If the sender's Relayer cannot find any matching PSI points from any other relayer, they invite the recipient to create an account with them.

Related code: AccountCreation circuit.

Relayer Incentives

Relayers pay the gas for creating the account and executing EmailOps. To incentivize the Relayer to do this, we have a fee reimbursement mechanism.

Relayers can set feeToken and feePerGas values in the EmailOp (below the max value allowed in the Core contract). After each EmailOp, the Core contract reimburses the relayer with feePerGas * gasUsed amount of ETH equivalent in feeToken.

Relayers profit from the difference between feePerGas in the EmailOp and the actual market gas fee.

Core contract is designed to do fee reimbursement even if an EmailOp execution fails (for example, due to some error in an extension). But if a transaction fails in the validation phase, the relayer is not reimbursed. To prevent this, Relayer should dry-run a transaction before executing it on-chain. A transaction passing locally is expected to pass on-chain.

Relayer pays the fee for creating/initializing new accounts though. To prevent DOS attacks, Relayers can have necessary checks - for example, create accounts only for users who have registered an UnclaimedFund with a minimum amount.

Relevant code handleEmailOp in EmailWalletCore.

On-chain DKIM Registry

Proof verification involves verifying that the DKIM public key used when generating the proof is the same as the one stored on-chain before (in a DKIM Registry). This adds a trust assumption on the DKIM Registry and persons(s) who have control over updating public keys for a domain.

As a solution, we allow Email Wallet users to use a custom DKIM Registry to verify their emails.

Users can deploy their own DKIM registry and set the public keys for their email domain. They can then send an email with a subject like DKIM registry set to 0x1ababab with their registry contract address.

We use the hash of DKIMPublicKey as the circuit output (and in the registry) instead of the public key directly to save on gas costs (as the public key is large).

Relevant code AccountHandler.sol.

EIP-4337

Account Abstraction EIP-4337 was considered for Email Wallet. However, it is not implemented in the current version of Email Wallet.

Email Wallet requires a Relayer to listen to emails from the user (IMAP) and generate the proof of email. This is not something a 4337 bundler can do.

A design where Relayer generates the proof and calls the 4337 Bundler with UserOp can be done (with some hacks to overcome 4337 storage restrictions), but this doesn't offer a lot of advantages. One advantage would be ensuring simulated transactions also pass during execution, though chances of this happening otherwise are also very low.

Nonetheless, a 4337 account for Email Wallet can be explored in the future if the ecosystem offers a lot of value.

Client-side proving

Many of the above restrictions are to force the Relayer to be honest and censorship-resistant. If we can have the emails proven on the client side (browser), we can skip the Relayer and have the user broadcast transactions directly.

For this, a 4337 wallet could be explored, where the user's browser calls 4337 Bundler with proof of email as the UserOp signature. The account key can be a PIN code entered by the user and stored in the browser.

However, client-side proving will require the user to copy the whole email content and paste it into a web app. This is a bad UX considering sending money is a frequent use-case and demands a simple UX that also works from mobile.

On the other hand, users could self-host Relayer on their own computer to avoid trusting a third party.


Conclusion

Email Wallet has the potential to onboard many new users to Ethereum. Users can interact with Ethereum without knowing anything about wallets, private keys, gas, etc.

Email Wallet is a gateway to Ethereum, and not just a simple way to send money. Developers can build extensions to allow users to interact with their smart contracts by sending emails. This can be used to build many interesting applications, apart from interactions with Defi contracts.

For example, Email Wallet could be used as a recovery solution for other contract wallets. Or email could be used as one of the keys for a multisig. (We are exploring more on this with other teams.)

You can learn more about Email Wallet circuits here, and the contracts here.

If you are interested in building on top of ZK Email or Email Wallet, please join our Telegram group.