yarn install
- Newer than or equal to
forge 0.2.0 (13497a5)
.
Make sure you have Foundry installed
Build the contracts using the below command.
$ yarn build
Run unit tests
$ yarn test
Run integration tests
Before running integration tests, you need to make a packages/contracts/test/build_integration
directory, download the zip file from the following link, and place its unzipped directory under that directory.
https://drive.google.com/file/d/1XDPFIL5YK8JzLGoTjmHLXO9zMDjSQcJH/view?usp=sharing
Then, move email_auth_with_body_parsing_with_qp_encoding.zkey
and email_auth_with_body_parsing_with_qp_encoding.wasm
in the unzipped directory params
to build_integration
.
Run each integration tests one by one as each test will consume a lot of memory.
Eg: contracts % forge test --skip '*ZKSync*' --match-contract "IntegrationTest" -vvv --chain 8453 --ffi
You need to deploy common contracts, i.e., ECDSAOwnedDKIMRegistry
, Verifier
, and implementations of EmailAuth
and SimpleWallet
, only once before deploying each wallet.
cp .env.sample .env
.- Write your private key in hex to the
PRIVATE_KEY
field in.env
. If you want to verify your own contracts, you can setETHERSCAN_API_KEY
to your own key. source .env
forge script script/DeployCommons.s.sol:Deploy --rpc-url $RPC_URL --chain-id $CHAIN_ID --etherscan-api-key $ETHERSCAN_API_KEY --broadcast --verify -vvvv
After deploying common contracts, you can deploy a proxy contract of SimpleWallet
, which is an example contract supporting our email-based account recovery by RecoveryController
.
- Check that the env values of
DKIM
,VERIFIER
,EMAIL_AUTH_IMPL
, andSIMPLE_WALLET_IMPL
are the same as those output by theDeployCommons.s.sol
script. forge script script/DeployRecoveryController.s.sol:Deploy --rpc-url $RPC_URL --chain-id $CHAIN_ID --broadcast -vvvv
There are four main contracts that developers should understand: IDKIMRegistry
, Verifier
, EmailAuth
and EmailAccountRecovery
.
While the first three contracts are agnostic to use cases of our SDK, the last one is an abstract contract only for our email-based account recovery.
It is an interface of the DKIM registry contract that traces public keys registered for each email domain in DNS.
It is defined in the zk-email library.
It requires a function isDKIMPublicKeyHashValid(string domainName, bytes32 publicKeyHash) view returns (bool)
: it returns true if the given hash of the public key publicKeyHash
is registered for the given email-domain name domainName
.
One of its implementations is ECDSAOwnedDKIMRegistry
.
It stores the Ethereum address signer
who can update the registry.
We also provide another implementation called UserOverrideableDKIMRegistry
.
Key features of UserOverrideableDKIMRegistry
include:
- User-specific public key setting
- Time-delayed activation of main authorizer's approvals
- Dual revocation mechanism (user or main authorizer)
- Reactivation of revoked keys (by users only)
This implementation provides a balance between centralized management and user autonomy in the DKIM registry system.
It has the responsibility to verify a ZK proof for the email_auth_with_body_parsing_with_qp_encoding.circom
circuit.
It is implemented in utils/Verifier.sol
.
It defines a structure EmailProof
consisting of the ZK proof and data of the instances necessary for proof verification as follows:
struct EmailProof {
string domainName; // Domain name of the sender's email
bytes32 publicKeyHash; // Hash of the DKIM public key used in email/proof
uint timestamp; // Timestamp of the email
string maskedCommand; // Masked command of the email
bytes32 emailNullifier; // Nullifier of the email to prevent its reuse.
bytes32 accountSalt; // Create2 salt of the account
bool isCodeExist; // Check if the account code exists
bytes proof; // ZK Proof of Email
}
Using that, it provides a function function verifyEmailProof(EmailProof memory proof) public view returns (bool)
: it takes as input the EmailProof proof
and returns true if the proof is valid. Notably, it internally calls Groth16Verifier.sol
generated by snarkjs from the verifying key of the email_auth_with_body_parsing_with_qp_encoding.circom
circuit.
It is a contract deployed for each email user to verify an email-auth message from that user. The structure of the email-auth message is defined as follows:
struct EmailAuthMsg {
uint templateId; // The ID of the command template that the email command should satisfy.
bytes[] commandParams; // The parameters in the email command, which should be taken according to the specified command template.
uint skippedCommandPrefix; // The number of skipped bytes in the email command.
EmailProof proof; // The email proof containing the zk proof and other necessary information for the email verification by the verifier contract.
}
It has the following storage variables.
address owner
: an address of the contract owner.bytes32 accountSalt
: anaccountSalt
used for the CREATE2 salt of this contract.DKIMRegistry dkim
: an instance of the DKIM registry contract.Verifier verifier
: an instance of the Verifier contract.address controller
: an address of a controller contract, defining the command templates supported by this contract.mapping(uint=>string[]) commandTemplates
: a mapping of the supported command templates associated with its ID.mapping(bytes32⇒bytes32) authedHash
: a mapping of the hash of the authorized message associated with itsemailNullifier
.uint lastTimestamp
: the latesttimestamp
in the verifiedEmailAuthMsg
.mapping(bytes32=>bool) usedNullifiers
: a mapping storing the usedemailNullifier
bytes.bool timestampCheckEnabled
: a boolean whether timestamp check is enabled or not.
It provides the following functions.
initialize(address _initialOwner, bytes32 _accountSalt, address _controller)
- Set
owner=_initialOwner
. - Set
accountSalt=_accountSalt
. - Set
timestampCheckEnabled=true
. - Set
controller=_controller
.
- Set
dkimRegistryAddr() view returns (address)
Returnaddress(dkim)
verifierAddr() view returns (address)
Returnaddress(verifier)
.initDKIMRegistry(address _dkimRegistryAddr)
- Assert
msg.sender==controller
. - Assert
dkim
is zero. - Set
dkim=IDKIMRegistry(_dkimRegistryAddr)
.
- Assert
initVerifier(address _verifierAddr)
- Assert
msg.sender==controller
. - Assert
verifier
is zero. - Set
verifier=Verifier(_verifierAddr)
.
- Assert
updateDKIMRegistry(address _dkimRegistryAddr)
- Assert
msg.sender==owner
. - Assert
_dkimRegistryAddr
is not zero. - Set
dkim=DKIMRegistry(_dkimRegistryAddr)
.
- Assert
updateVerifier(address _verifier)
- Assert
msg.sender==owner
. - Assert
_verifier
is not zero. - Set
verifier=Verifier(_verifier)
.
- Assert
updateVerifier(address _verifierAddr)
- Assert
msg.sender==owner
. - Assert
_verifierAddr!=0
. - Update
verifier
toVerifier(_verifierAddr)
.
- Assert
updateDKIMRegistry(address _dkimRegistryAddr)
- Assert
msg.sender==owner
. - Assert
_dkimRegistryAddr!=0
. - Update
dkim
toDKIMRegistry(_dkimRegistryAddr)
.
- Assert
getCommandTemplate(uint _templateId) public view returns (string[] memory)
- Assert that the template for
_templateId
exists, i.e.,commandTemplates[_templateId].length >0
holds. - Return
commandTemplates[_templateId]
.
- Assert that the template for
insertCommandTemplate(uint _templateId, string[] _commandTemplate)
- Assert
_commandTemplate.length>0
. - Assert
msg.sender==controller
. - Assert
commandTemplates[_templateId].length == 0
, i.e., no template has not been registered with_templateId
. - Set
commandTemplates[_templateId]=_commandTemplate
.
- Assert
updateCommandTemplate(uint _templateId, string[] _commandTemplate)
- Assert
_commandTemplate.length>0
. - Assert
msg.sender==controller
. - Assert
commandTemplates[_templateId].length != 0
, i.e., any template has been already registered with_templateId
. - Set
commandTemplates[_templateId]=_commandTemplate
.
- Assert
deleteCommandTemplate(uint _templateId)
- Assert
msg.sender==controller
. - Assert
commandTemplates[_templateId].length > 0
, i.e., any template has been already registered with_templateId
. delete commandTemplates[_templateId]
.
- Assert
authEmail(EmailAuthMsg emailAuthMsg) returns (bytes32)
- Assert
msg.sender==controller
. - Let
string[] memory template = commandTemplates[emailAuthMsg.templateId]
. - Assert
template.length > 0
. - Assert
dkim.isDKIMPublicKeyHashValid(emailAuthMsg.proof.domain, emailAuthMsg.proof.publicKeyHash)==true
. - Assert
usedNullifiers[emailAuthMsg.proof.emailNullifier]==false
and setusedNullifiers[emailAuthMsg.proof.emailNullifier]
totrue
. - Assert
accountSalt==emailAuthMsg.proof.accountSalt
. - If
timestampCheckEnabled
is true, assert thatemailAuthMsg.proof.timestamp
is zero ORlastTimestamp < emailAuthMsg.proof.timestamp
, and updatelastTimestamp
toemailAuthMsg.proof.timestamp
. - Construct an expected command
expectedCommand
fromtemplate
and the values ofemailAuthMsg.commandParams
. - Assert that
expectedCommand
is equal toemailAuthMsg.proof.maskedCommand[skippedCommandPrefix:]
, i.e., the string ofemailAuthMsg.proof.maskedCommand
from theskippedCommandPrefix
-th byte. - Assert
verifier.verifyEmailProof(emailAuthMsg.proof)==true
.
- Assert
isValidSignature(bytes32 _hash, bytes memory _signature) public view returns (bytes4 magicValue)
- Parse
_signature
as(bytes32 emailNullifier)
. - If
authedHash[emailNullifier]== _hash
, return0x1626ba7e
; otherwise return0xffffffff
.
- Parse
setTimestampCheckEnabled(bool enabled) public
- Assert
msg.sender==controller
. - Set
timestampCheckEnabled
toenabled
.
- Assert
It is an abstract contract for each smart account brand to implement the email-based account recovery. Each smart account provider only needs to implement the following functions in a new contract called controller. In the following, the templateIdx
is different from templateId
in the email-auth contract in the sense that the templateIdx
is an incremental index defined for each of the command templates in acceptanceCommandTemplates()
and recoveryCommandTemplates()
.
isActivated(address recoveredAccount) public view virtual returns (bool)
: it returns if the account to be recovered has already activated the controller (the contract implementingEmailAccountRecovery
).acceptanceCommandTemplates() public view virtual returns (string[][])
: it returns multiple command templates for an email to accept becoming a guardian (acceptance email).recoveryCommandTemplates() public view virtual returns (string[][])
: it returns multiple command templates for an email to confirm the account recovery (recovery email).extractRecoveredAccountFromAcceptanceCommand(bytes[] memory commandParams, uint templateIdx) public view virtual returns (address)
: it takes as input the parameterscommandParams
and the index of the chosen command templatetemplateIdx
in those for acceptance emails.extractRecoveredAccountFromRecoveryCommand(bytes[] memory commandParams, uint templateIdx) public view virtual returns (address)
: it takes as input the parameterscommandParams
and the index of the chosen command templatetemplateIdx
in those for recovery emails.acceptGuardian(address guardian, uint templateIdx, bytes[] commandParams, bytes32 emailNullifier) internal virtual
: it takes as input the Ethereum addressguardian
corresponding to the guardian's email address, the indextemplateIdx
of the command template in the output ofacceptanceCommandTemplates()
, the parameter values of the variable partscommandParams
in the templateacceptanceCommandTemplates()[templateIdx]
, and an email nullifieremailNullifier
. It is called after verifying the email-auth message to accept the role of the guardian; thus you can assume the arguments are already verified.processRecovery(address guardian, uint templateIdx, bytes[] commandParams, bytes32 emailNullifier) internal virtual
: it takes as input the Ethereum addressguardian
corresponding to the guardian's email address, the indextemplateIdx
of the command template in the output ofrecoveryCommandTemplates()
, the parameter values of the variable partscommandParams
in the templaterecoveryCommandTemplates()[templateIdx]
, and an email nullifieremailNullifier
. It is called after verifying the email-auth message to confirm the recovery; thus you can assume the arguments are already verified.completeRecovery(address account, bytes memory completeCalldata) external virtual
: it can be called by anyone, in particular a Relayer, when completing the account recovery. It should first check if the condition for the recovery ofaccount
holds and then update its owner's address in the wallet contract.
It also provides the following entry functions with their default implementations, called by the Relayer.
handleAcceptance(EmailAuthMsg emailAuthMsg, uint templateIdx) external
- Extract an account address to be recovered
recoveredAccount
by callingextractRecoveredAccountFromAcceptanceCommand
. - Let
address guardian = CREATE2(emailAuthMsg.proof.accountSalt, ERC1967Proxy.creationCode, emailAuthImplementation(), (emailAuthMsg.proof.accountSalt))
. - Let
uint templateId = keccak256(EMAIL_ACCOUNT_RECOVERY_VERSION_ID, "ACCEPTANCE", templateIdx)
. - Assert that
templateId
is equal toemailAuthMsg.templateId
. - Assert that
emailAuthMsg.proof.isCodeExist
is true. - If the
EmailAuth
contract ofguardian
has not been deployed, deploy the proxy contract ofemailAuthImplementation()
. Its salt isemailAuthMsg.proof.accountSalt
and its initialization parameter isrecoveredAccount
,emailAuthMsg.proof.accountSalt
, andaddress(this)
, which is a controller of the deployed contract. - If the
EmailAuth
contract ofguardian
has not been deployed, callEmailAuth(guardian).initDKIMRegistry(dkim())
. - If the
EmailAuth
contract ofguardian
has not been deployed, callEmailAuth(guardian).initVerifier(verifier())
. - If the
EmailAuth
contract ofguardian
has not been deployed, for eachtemplate
inacceptanceCommandTemplates()
along with its indexidx
, callEmailAuth(guardian).insertCommandTemplate(keccak256(EMAIL_ACCOUNT_RECOVERY_VERSION_ID, "ACCEPTANCE", idx), template)
. - If the
EmailAuth
contract ofguardian
has not been deployed, for eachtemplate
inrecoveryCommandTemplates()
along with its indexidx
, callEmailAuth(guardian).insertCommandTemplate(keccak256(EMAIL_ACCOUNT_RECOVERY_VERSION_ID, "RECOVERY", idx), template)
. - If the
EmailAuth
contract ofguardian
has been already deployed, assert that itscontroller
is equal toaddress(this)
. - Assert that
EmailAuth(guardian).authEmail(emailAuthMsg)
returns no error. - Call
acceptGuardian(guardian, templateIdx, emailAuthMsg.commandParams, emailAuthMsg.proof.emailNullifier)
.
- Extract an account address to be recovered
handleRecovery(EmailAuthMsg emailAuthMsg, uint templateIdx) external
- Extract an account address to be recovered
recoveredAccount
by callingextractRecoveredAccountFromRecoveryCommand
. - Let
address guardian = CREATE2(emailAuthMsg.proof.accountSalt, ERC1967Proxy.creationCode, emailAuthImplementation(), (emailAuthMsg.proof.accountSalt))
. - Assert that the contract of
guardian
has been already deployed. - Let
uint templateId=keccak256(EMAIL_ACCOUNT_RECOVERY_VERSION_ID, "RECOVERY", templateIdx)
. - Assert that
templateId
is equal toemailAuthMsg.templateId
. - Assert that
EmailAuth(guardian).authEmail(emailAuthMsg)
returns no error. - Call
processRecovery(guardian, templateIdx, emailAuthMsg.commandParams, emailAuthMsg.proof.emailNullifier)
.
- Extract an account address to be recovered
# Install foundry
foundryup
cd packages/contracts
yarn build
# Install foundry-zksync, please follow this URL
https://foundry-book.zksync.io/getting-started/installation
# Install era-test-node
https://github.com/matter-labs/era-test-node
Next, you should uncomment the following lines in foundry.toml
.
# via-ir = true
Partial comment-out files can be found the following. Please uncomment them.
(Uncomment from FOR_ZKSYNC:START
to FOR_ZKSYNC:END
)
- src/utils/ZKSyncCreate2Factory.sol
- test/helpers/DeploymentHelper.sol
Run the era-test-node forking zksync sepolia
era_test_node fork https://sepolia.era.zksync.dev
At the first forge build, you need to detect the missing libraries.
forge build --zksync --zk-detect-missing-libraries
As you saw before, you need to deploy missing libraries. You can deploy them by the following command for example.
$ forge build --zksync --zk-detect-missing-libraries
Missing libraries detected: src/libraries/CommandUtils.sol:CommandUtils, src/libraries/DecimalUtils.sol:DecimalUtils, src/libraries/StringUtils.sol:StringUtils
Run the following command in order to deploy each missing libraries:
export PRIVATE_KEY={YOUR_PRIVATE_KEY}
export RPC_URL=http://127.0.0.1:8011
export CHAIN_ID=260
forge create src/libraries/DecimalUtils.sol:DecimalUtils --private-key $PRIVATE_KEY --rpc-url $RPC_URL --chain $CHAIN_ID --zksync
forge create src/libraries/CommandUtils.sol:CommandUtils --private-key $PRIVATE_KEY --rpc-url $RPC_URL --chain $CHAIN_ID --zksync --libraries src/libraries/DecimalUtils.sol:DecimalUtils:{DECIMAL_UTILS_ADDRESS_YOU_DEPLOYED}
forge create src/libraries/StringUtils.sol:StringUtils --private-key $PRIVATE_KEY --rpc-url $RPC_URL --chain $CHAIN_ID --zksync
After that, you can see the following lines in the foundry.toml. Please replace {PROJECT_DIR}
and {DEPLOYED_ADDRESS}
.
Also, this lines are needed only for foundry-zksync, if you use normal foundry commands, please comment out.
libraries = [
"{PROJECT_DIR}/packages/contracts/src/libraries/DecimalUtils.sol:DecimalUtils:{DEPLOYED_ADDRESS}",
"{PROJECT_DIR}/packages/contracts/src/libraries/CommandUtils.sol:CommandUtils:{DEPLOYED_ADDRESS}"
"{PROJECT_DIR}/packages/contracts/src/libraries/StringUtils.sol:StringUtils:{DEPLOYED_ADDRESS}"
]
About Create2, L2ContractHelper.computeCreate2Address
should be used.
type(ERC1967Proxy).creationCode
doesn't work correctly in ZKsync.
We need to use the bytecode hash intead of type(ERC1967Proxy).creationCode
.
Perhaps that is a different value in each compiler version and library addresses.
Run the following commands, you'll get the bytecode hash.
forge test --match-test "testComputeCreate2Address" --no-match-contract ".*Script.*" --system-mode=true --zksync --gas-limit 1000000000 --chain 300 -vvv --fork-url http://127.0.0.1:8011
And then, you should replace {YOUR_BYTECODE_HASH}
in the .env
PROXY_BYTECODE_HASH={YOUR_BYTECODE_HASH}
Run the following commands
source .env
yarn zktest
Even if the contract size is fine for EVM, it may exceed the bytecode size limit for zksync, and the test may not be executed. If you encountered the contract size error, please consider the contract design.
source .env
forge test --match-contract "IntegrationZKSyncTest" --system-mode=true --zksync --gas-limit 1000000000 --chain 300 -vvv --ffi
As you saw before, you need to deploy missing libraries. You can deploy them by the following commands for example.
export PRIVATE_KEY={YOUR_PRIVATE_KEY}
export RPC_URL=https://sepolia.era.zksync.dev
export CHAIN_ID=300
forge create src/libraries/DecimalUtils.sol:DecimalUtils --private-key $PRIVATE_KEY --rpc-url $RPC_URL --chain $CHAIN_ID --zksync
forge create src/libraries/CommandUtils.sol:CommandUtils --private-key $PRIVATE_KEY --rpc-url $RPC_URL --chain $CHAIN_ID --zksync --libraries src/libraries/DecimalUtils.sol:DecimalUtils:{DECIMAL_UTILS_ADDRESS_YOU_DEPLOYED}
forge create src/libraries/StringUtils.sol:StringUtils --private-key $PRIVATE_KEY --rpc-url $RPC_URL --chain $CHAIN_ID --zksync
And then you need to replace {PROJECT_DIR}
and {DEPLOYED_ADDRESS}
in the foundy.toml.
libraries = [
"{PROJECT_DIR}/packages/contracts/src/libraries/DecimalUtils.sol:DecimalUtils:{DEPLOYED_ADDRESS}",
"{PROJECT_DIR}/packages/contracts/src/libraries/CommandUtils.sol:CommandUtils:{DEPLOYED_ADDRESS}"]
"{PROJECT_DIR}/packages/contracts/src/libraries/StringUtils.sol:StringUtils:{DEPLOYED_ADDRESS}"
Run this command again, you'll get the bytecode hash.
forge test --match-test "testComputeCreate2Address" --no-match-contract ".*Script.*" --system-mode=true --zksync --gas-limit 1000000000 --chain 300 -vvv --fork-url http://127.0.0.1:8011
And then, you should replace {YOUR_BYTECODE_HASH}
in the .env
PROXY_BYTECODE_HASH={YOUR_BYTECODE_HASH}
Run the deploy script
source .env
export RPC_URL=https://sepolia.era.zksync.dev
export CHAIN_ID=300
forge script script/DeployRecoveryControllerZKSync.s.sol:Deploy --zksync --rpc-url $RPC_URL --broadcast --slow --via-ir --system-mode true -vvvv --verifier zksync --verifier-url https://explorer.sepolia.era.zksync.dev/contract_verification --verify
For information on how upgrade contracts for handling emergency situations, please refer to our Upgrading Contracts.