Our team is currently working on this feature. If you experience any bugs, please let us know on our Discord. We appreciate your patience.
Email Significance
Many online services, from social media platforms to e-commerce sites, require an email address to create an account. According to recent surveys, more than 80% of businesses consider email to be their primary communication channel, both internally and with customers.
All of this means that our inboxes are full of data that can be leveraged.
Proof of Email
With vlayer, you can access email content from smart contracts and use it on-chain.
You do this by writing a Solidity smart contract (Prover
) that has access to the parsed email and returns data to be used on-chain. This allows you to create claims without exposing the full content of an email.
Under the hood, we verify mail server signatures to ensure the authenticity and integrity of the content.
Example
Let's say someone wants to prove they are part of company or organization. One way to do this is to take a screenshot and send it to the verifier. However, this is not very reliable because screenshot images can be easily manipulated, and obviously such an image cannot be verified on-chain.
A better option is to prove that one can send email from it's organization domain. Below is a sample Prover
contract that verifies that the sender sent email from a specific domain.
Below is an example of such proof generation:
import {Strings} from "@openzeppelin-contracts-5.0.1/utils/Strings.sol";
import {Prover} from "vlayer-0.1.0/src/Prover.sol";
import {VerifiedEmail, UnverifiedEmail, EmailProofLib} from "vlayer-0.1.0/src/EmailProof.sol";
import {EmailStrings} from "./EmailStrings.sol";
contract EmailDomainProver is Prover {
using Strings for string;
using EmailStrings for string;
using EmailProofLib for UnverifiedEmail;
string targetDomain;
constructor(string memory _targetDomain) {
targetDomain = _targetDomain;
}
function main(UnverifiedEmail calldata unverifiedEmail, address targetWallet)
public
view
returns (Proof, bytes32, address)
{
VerifiedEmail memory email = unverifiedEmail.verify();
require(email.from.contains(targetDomain), "incorrect sender domain");
require(email.subject.equal("Verify me for company NFT"), "incorrect subject");
return (proof(), sha256(abi.encodePacked(email.from)), targetWallet);
}
}
It can be convenient to use Regular Expressions to validate the content of the email.
Email is passed to the Solidity contract as an UnverifiedEmail
structure that can be created using the preverifyEmail
function in the SDK.
preverifyEmail
should be called with the raw .eml
file content as an argument. The email is also required to have "From" and "DKIM-Signature" headers.
// Note: more fields will be added soon
struct UnverifiedEmail {
string email;
string[] dnsRecords;
}
First, we verify the integrity of the email with the verify()
function. Then we have a series of assertions (regular Solidity require()
) that check the email details.
String comparison is handled by our StringUtils
library (described in more details below). Date values are formatted in the Unix time notation, which allows them to be compared as integers.
If one of the string comparisons fails, require will revert the execution, and as a result, proof generation will fail.
💡 Try it Now
To run the above example on your computer, type the following command in your terminal:
vlayer init --template email-proof
This command will download create and initialise a new project with sample email proof contracts.
Email structure
The email
structure of type VerifiedEmail
is injected into the Prover
and can be used in a main()
function.
struct VerifiedEmail {
string subject;
string body;
string from;
string to;
}
An VerifiedEmail
consists of the following fields
subject
- a string with the subject of the emailbody
- a string consisting of the entire body of the emailfrom
- a string consisting of the sender's email address (no name is available)to
- a string consisting of the intended recipient's email address (no name is available)
By inspecting and parsing the email payload elements, we can generate a claim to be used on-chain.
StringUtils
For convenient manipulation of strings, vlayer provides StringUtils
library, which consists of functions like:
toAddress
- converts a string to an address if properly formatted, reverts otherwisematch
- matches RegExp pattern groups and returns them as a stringequal
- checks the contents of two strings for equality. Returns true if both are equal, false otherwise.
Wallet Recovery Example
Below is another example of a Prover
smart contract parsing an email. This time, however, the use case is a bit more advanced. It allows the caller to recover access to a MultiSig wallet (a smart contract that allows multiple wallets to authorize transactions).
The following implementation assumes that the recovery email is in a predefined format. It extracts the data needed to restore access to a MultiSig wallet.
To change the authorized account (recovery procedure), the user simply needs to send the email in the following format:
Date: 02 Jul 24 14:52:18+0300 GMT
From: [email protected]
To: <any email with trusted provider>
Subject: Wallet recovery of {old account address}
Body: New wallet address {new account address}
Now, we can access the email from the Prover
contract:
import {Prover} from "vlayer/Prover.sol";
import {VerifiedEmail, UnverifiedEmail, EmailProofLib} from "vlayer/EmailProof.sol";
import {StringUtils} from "vlayer/StringUtils.sol"
contract RecoveryEmail is Prover {
using StringUtils for string;
using EmailProofLib for UnverifiedEmail;
function main(address multisigAddr, UnverifiedEmail calldata unverifiedEmail) public returns (address, bytes32, address) {
VerifiedEmail memory email = unverifiedEmail.verify()
address lostWallet = parseSubject(email.subject);
address newAddress = parseBody(email.body);
bytes32 emailAddrHash = getEmailAddressHash(email.from, multisigAddr, lostWallet);
return (lostWallet, emailAddrHash, newAddress);
}
function parseSubject(string calldata subject) internal returns (address) {
string[] subjectMatches = subject.match(
"^Wallet recovery of (0x[a-fA-F0-9]{40})$"
);
require(subjectMatches.length == 1, "Invalid subject");
return subjectMatches[0].toAddress();
}
function parseBody(string calldata body) internal returns (address) {
string[] bodyMatches = body.match(
"^New wallet address: (0x[a-fA-F0-9]{40})$"
);
require(bodyMatches.length == 1, "Invalid body");
return newbodyMatches[0].toAddress();
}
function getEmailAddressHash(string calldata emailAddr, address multisig, address owner)
internal
returns (bytes32)
{
MultiSigWallet wallet = MultiSigWallet(multisig);
bytes32 memory recoveryMailHash = wallet.recoveryEmail(owner);
bytes32 emailAddrHash = keccak256(abi.encodePacked(emailAddr);
require(recoveryMailHash == emailAddrHash, "Recovery email mismatch");
return emailAddrHash;
}
}
What happens step by step in the above snippet?
RecoveryEmail
inherits fromProver
to obtain super powers of off-chain proving.main
function takesmultisigAddr
argument to access Multisig Wallet smart contract data.parseSubject
parses email subject and returns address of lost walletemail.subject.match
returns strings matching the regular expression for the subject, which must contain the correct wallet address to be recovered.- The
subjectMatches.length == 1
condition ensures that the subject is not malformed.
parseBody
extracts new owner addressemail.body.match
retrieves new wallet address from the email body
getEmailAddressHash
compares the email address associated with the wallet with the one received.recoveryMailHash == emailAddrHash
check if correct email was used for recovery
On successful execution, proof of computation is returned. It also returns the recovered wallet address, the email address hash, the new wallet address, and the email timestamp as public input.
Verifier
Now we are ready to use the proof and results from the previous step for on-chain verification. Valid proof allows us to restore access to MultiSigWallet
by adding new address to authorized list in smart contract.
Below is a sample implementation of this:
import { Verifier } from "vlayer/Verifier.sol";
import { RecoveryEmail } from "RecoveryEmail.sol";
address constant PROVER_ADDR = address(0xd7141F4954c0B082b184542B8b3Bd00Dc58F5E05);
bytes4 constant PROVER_FUNC_SELECTOR = RecoveryEmail.main.selector;
contract MultiSigWallet is Verifier {
mapping (address => bool) public owners;
mapping (address => bytes32) ownerToEmailHash;
function recovery(
Proof _p,
address lostWallet,
bytes32 emailAddrHash,
address newOwner,
)
public
onlyVerified(PROVER_ADDR, PROVER_FUNC_SELECTOR)
returns (address)
{
require(
ownerToEmailHash[lostWallet] == emailAddrHash,
"Recovery email mismatch"
);
require(owners[lostWallet]; "Not an owner");
owners[lostWallet] = false;
owners[newOwner] = true;
return (newOwner);
}
}
What exactly happened in the above code?
-
First, note we need to let know
Verifier
, whichProver
contract to verify:- The
PROVER_ADDR
constant holds the address of theProver
contract that generated the proof. - The
PROVER_FUNC_SELECTOR
constant holds the selector for theProver.main()
function. MultiSigWallet
inherits fromVerifier
, so we can call theonlyVerified
modifier that makes sure the proof is correct or it will revert otherwise.
- The
-
Next, we add two fields that an example MultiSig might use:
- The
owners
mapping holds addresses that can useMultiSigWallet
. - The
ownerToEmailHash
mapping holds hashes of email addresses associated with owners.
- The
-
Finally, we need a logic that will perform all verifications and do actual recovery:
- The
recovery()
function takes follwoing arguments:proof
and returned values generated byProver.main()
. onlyVerified(PROVER_ADDR, PROVER_FUNC_SELECTOR)
validates execution of Prover and correctness of arguments. If the proof is invalid or arguments don't match returned values it will revert.- You don't need to pass
proof
as an argument toonlyVerified
because it is automatically extracted frommsg.data
. ownerToEmailHash[lostWallet] == emailAddrHash
make sure recovery email address matches the one that was set up previously in the walletowners[newOwner] = true
sets up a new wallet to be authorized to useMultiSigWallet
.
- The
And voilà, we just successfully used email in the context of an on-chain smart contract.
Keep in mind that this is a simplified version of a real MultiSig wallet, demonstrating how an email recovery function could operate.
Security Assumptions
Billions of users trust providers to deliver and store their emails. Inboxes often contain critical information, including work-related data, personal files, password recovery links, and more. Email providers also access customer emails for purposes like serving ads. Email proofs can only be as secure as the email itself, and the protocol relies on the trustworthiness of both sending and receiving servers.
Outgoing Server
The vlayer prover verifies that the message signature matches the public key listed in the DNS records. However, a dishonest outgoing server can forge emails and deceive the prover into generating valid proofs for them. To mitigate this risk, vlayers support only a limited number of the world's most trusted email providers.
Preventing Unauthorized Actions
Both outgoing and incoming servers can read emails and use them to create proofs without the permission of the actual mail sender or receiver. This risk also extends to the prover, which accesses the email to generate claims. It is crucial for protocols to utilize email proofs in a manner that prevents the manipulation of smart contracts into performing unauthorized actions, such as sending funds to unintended recipients.
For example, it is advisable to include complete information in the email to ensure correct actions. Opt for emails like: "Send 1 ETH from address X to address Y on Ethereum Mainnet" over partial instructions, like: "Send 1 ETH," where other details come from another source, such as smart contract call parameters. Another approach is to use unique identifiers that unambiguously point to the necessary details.