Author: Jagrut Kosti
Date: Nov 23, 2022
ENS names act as a human readable representation of an Ethereum address that they can exchange or broadcast in order to receive funds, have their social media information at one place, or any other text records associated with the user. While convenient, it certainly is bad for privacy. It is a common practice to have your ENS published on social media where everyone can see it. This also means anyone can lookup who is sending funds to a user and how they are spending their funds.
ENS privacy dilemma is interesting! On one hand, we want to have a convenient means of sharing the address where anyone can send assets and on the other, we do not want any and everyone to watch our on-chain activity associated to that address. With on-chain analysis tools, this can be extrapolated to create a graph and generate an in-depth analysis of spending patterns of the user.
We also want the wallet providers to seamlessly translate the name to an address so that they can construct an on-chain transaction. This needs to happen without the intervention of the receiver. ENS does that by providing callable methods on their contracts, which the wallet providers can query.
Essentially, what we need is:
In our research document we outlined a few approaches and possible directions that we can take. This document specifies the integration between Aztec and ENS and how this mitigates what we set out to solve.
For an in-depth understanding of ENS, please refer to their docs. In this section we highlight the aspects that we will utilize in our solution. The links reference the relevant source code.
For resolving a name for an address, the first query is made to the Registry
contract's resolver()
method. This will return the address of the resolver contract.
The second query is made on the PublicResolver contract's addr()
method. This returns the actual address associated with the ENS name which can be then used to form a transaction and send assets.
There is a possibility to set a custom resolver for a given name by the controller. By default, the PublicResolver
contract's address is set on ENS registration. Custom resolver can be set by calling Registry
contract's setResolver()
method. This is an important aspect that we will use in our design. This allows the current version of the ENS to run as is while allowing existing and new users of ENS to use our privacy focused custom resolver.
Aztec provides fully confidential Ethereum transactions and is based on ZK-Rollup and is secured by PLONK proving mechanism. For an in-depth understanding of Aztec, please refer to their docs. In this section we highlight the aspects that we will refer in our solution. Throughout the document, L1 refers to Ethereum mainnet and L2 refers to Aztec's rollup service.
Aztec's architecture has 3 main components:
RollupProcessor.sol
.Falafel and Halloumi can either be run on the same machine or a different one, doesn't matter. A UI for interacting with them is currently available at https://zk.money.
Falafel processes L2 transactions, constructs new rollups and publishes them on L1. It also listens for events on L1 and makes the state changes on L2 accordingly. Each rollup can have a maximum of 896 transactions per rollup. If a user wants to have faster confirmation, they can pay for the "empty space" of a rollup and ask the rollup service to submit immediately.
Aztec works similar to UTXO model where instead of account having balances, there are owners of notes. More details here.
Ethereum accounts cannot be used to sign transactions on Aztec as it uses a different curve, Grumpkin curve.
A user in Aztec have two different key pairs, account(view) key and spending(signer) key. There is also a 3rd key, recovery key, but that is not relevant for our solution. Account key is used to decrypt the notes that are meant for them and spending key allows to spend those notes. There can be many spending keys that a user can register for one account.
Private part of all keys can be any random 32 bytes. But Aztec provides a way to deterministically derive those keys from a signed Ethereum message. More details here. This means, as long as users have their Ethereum private keys safe, they can derive Aztec keys. There is no requirement to additionally store different mnemonics or private keys for Aztec. Your existing wallets like Metamask and WalletConnect works seamlessly with Aztec.
Our solution is dependent on Aztec protocol. The solution is specific for ENS i.e how we can de-link receiving and spending addresses for users of ENS. For now, consider the following flow, explanation of each follows later:
To understand how the integration works, we will go through all steps that are required to accomplish our purpose.
For our entire solution, the sender need not register anywhere or use any specific protocol. This gives the flexibility for anyone on L1 to send funds to the receiver who wants to use our solution without the sender needing to register on any additional platforms.
Receiver should be registered on ENS and have the control to update the address of resolver contract. If a receiver is using some sub-domain, the controller for the domain should set the custom resolver address. For registering on ENS, follow the process as it is at: https://app.ens.domains/. After the registration is finished, update the resolver address to our custom resolver.
For transferring the funds from sender to receiver, neither of them needs to be registered on Aztec beforehand. But for the receiver to claim the funds, they need to register on Aztec. This can also be done later but we recommend doing it before hand. For registering on Aztec, follow the process as it is at: https://zk.money/. (NOTE: A minimum of 0.01 ETH is required for registration).
Most importantly, during registration of ENS, the address that gets resolved for an ENS name should be the same when registering with Aztec. All keys derived in Aztec should use the same address for signing the messages for key derivation.
We do not need any additional UI for our solution to work. The UIs from ENS and Aztec suffices.
This section describes the core of our proposed solution which ties ENS with Aztec.
As explained in section 2, the Resolver contract is the one responsible for finally resolving the address for a given ENS name. The default PublicResolver
contract is responsible for resolving everything related to the ENS name. A list of all profiles that are inherited by PublicResolver
is available here.
The base criteria for writing a new resolver is to have an implementation of the method:
function supportsInterface(bytes4 interfaceID) constant returns (bool);
More details available here.
For the custom resolver, we can have all the resolver profiles as it is in the default PublicResolver
except for the AddrResolver.sol
. Mainly, what the CustomAddrResolver modifies is:
sendPrivate()
, for forwarding the call to Aztec's RollupProcessor's
depositPendingFunds()
. (Explained in next section)For consistency, we use the same terminology as in ENS. E.g. Node: A cryptographic hash uniquely identifying a name. All terminology is available here.
A function signature code for the CustomAddrResolver.sol is as follows:
pragma solidity >=0.8.4;
abstract contract CustomAddrResolver {
/*
* Sets true for the user's node to enable accepting private transaction on Aztec.
*/
function setSendPrivate(bytes32 node, bool sendPrivate) external;
/*
* Resolves the ENS name to address and then constructs a call to RollupProcessor's
* depositPendingFunds() by setting the assetId and proofHash to 0, owner to receiver and amount to
* msg.value (in Wei) and forwards the call.
*
* This can be achieved either by having a contract ABI available when deploying this contract or hard
* coding the function signature and using abi.encodeWithSignature() with incoming parameters.
*
* @param node ENS name of the receiver
*/
function sendPrivate(bytes32 node) external payable;
/**
* Following are simply the requirement of EIP-137 that MUST be adhered
* https://github.com/ethereum/EIPs/blob/master/EIPS/eip-137.md
*/
event AddrChanged(bytes32 indexed node, address a);
function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool);
function addr(bytes32 node) constant returns(address);
function setAddr(bytes32 node, address addr);
function() {
throw;
}
}
The modified user flow of the ENS looks like:
Registry
contract's resolver()
method.addr()
function which will return the address mapped to ENS name.supportsPrivacy()
.sendPrivate()
function with the required parameters.Steps 1-3 are read-only calls and are enough to resolve the address for the given name. Step 5 is where the actual transfer of funds is done.
Step 4 & 5 will have to be done manually by the sender, not a good thing for the UX. We can:
Option 1: Provide another helper method in CustomAddrResolver
which will return the hex encoded form of the function call which the sender can simply paste in the "Hex Data" field of wallet providers when sending the transaction. This still requires the sender to call the helper method OR if wallet providers find this solution interesting, they can inherently include it as a part of their release.
Option 2 (Specific to Metamask): Create a custom Snap. The Snap will watch for the custom resolver address when an ENS name gets resolved. If it matches the address of our privacy enabled resolver, it will construct a transaction to send to sendPrivate()
function call and prompt the sender to sign using their Metamask.
Option 3: Create a standardization by introducing a new EIP. Once a wallet code identifies that a resolver supports privacy (e.g. using supportsPrivacy()
), the wallet should be forced to send funds through sendPrivate()
. The implementation details of sendPrivate()
can be different for different resolvers in case more people come up with new ideas to add some privacy on ENS.
Looking at Aztec's RollupProcessor
contract, we came across depositPendingFunds()
function:
function depositPendingFunds(
uint256 assetId, // For identifying ERC20 tokens, ETH = 0
uint256 amount, // Amount in Wei, should equal msg.value
address owner, // Who can claim this amount, L1 address
bytes32 proofHash // OPTIONAL, Submit the proof to make the *amount* spendable by the *owner*
) external payable;
This external payable function is callable by anyone. Originally, this was intended for anyone to deposit their funds from L1 to start using in L2. If you are the owner of the fund, you can submit the proof in the same transaction (saving some gas) to start using the funds in L2. Once the funds are approved, the owner can spend the associated zkETH / zkDAI on L2 or withdraw to a fresh address on L1.
Alternatively, a sender can send funds to this function using the receiver's address as the owner. Our custom resolver's sendPrivate()
function forwards the calls to this function. Falafel watches for the emitted events and the receiver can see there are pending funds whenever they log in to zkMoney's UI.
The receiver can later, asynchronously, submit the proof for their address using the approveProof()
function of RollupProcessor
contract.
function approveProof(bytes32 _proofHash) external;
If the receiver is using zkMoney's UI, they can simply "shield" their pending funds. The UI takes care of proof generation and submission, making the funds available to spend for the receiver.
Alternatively, this can be also done using the Aztec Connect SDK. If using the SDK with a custom DApp, there are several steps that needs to be done first. Installation steps can be found here. After that, they can use DepositController's
createProof()
method to generate the proof for the account on which the controller is instantiated.
Both the calls, from sender and receiver(proof submission), are made on the L1 contract and the gas needs to be paid in ETH. Sender will pay the gas cost when calling the sendPrivate()
function of the custom resolver, which in turn calls the depositPendingFunds()
on Aztec.
For approving the proof to make the funds spendable, the receiver bears the gas cost of the transaction when calling approveProof()
. This address is the public address used during registration and can be funded without any privacy concerns. The receiver need not call the proof for every transaction from the sender(s). They can wait as long as they want and cumulatively submit proof for all their pending funds in a single call, saving gas.
For the transactions made within L2, the gas costs are paid in zkETH by the receiver and are substantially lower than L1.
Cost of transactions:
Aztec guarantees private transactions within L2, meaning that a sender's identity or their balance is not visible to anyone, not even the recipient. Users can simply use the zkMoney's UI for withdrawal to L1 from L2. The UI creates withdraw proof using WithdrawController's
createProof()
for the account which is instantiated by the controller.
When withdrawing, users can use any address on L1, create and submit proof for that address and execute the transaction. Usually, single withdraw transactions will be expensive. Therefore, the rollup service provider waits for all the transactions within a rollup to fill up and then submit the rollup proof. For a block explorer on L1, the transaction will appear as though coming from the RollupProcessor
contract to the fresh address.
There are of course some caveats that the user needs to take care for protecting their privacy. E.g. not using the same registered address for withdrawal (duh!), not withdrawing any idiosyncratic amounts, etc. For more details on best practices on privacy sets, refer here.
When using the proposed solution for adding a layer of privacy integrated with ENS, consider the following aspects for someone observing both L1 and L2:
Publicly available information:
Non-linkable information:
This document describes one of the solution that we came up with which can be used to protect user's privacy i.e. how they spend their funds. Using Aztec, we established an integration with ENS's custom resolver contract. We established:
For a test implementation of this integration, the source code is available here.