UserOperation Sender
The sender field on a UserOperation is the address of a user's contract account on-chain.
The sender
fields is the address of a user's smart account that has been deployed through a factory contract.
Deterministic addresses
With EOAs, the address is consistent across all EVM networks. As long as a user has access to the private key they can access the same address on any network. Ideally we would also like to create the same user experience with contract accounts too.
A user should be able to deterministically know their smart account address and keep it consistent on every EVM network irrespective of whether the code
has been deployed or not. This means they can generate an account and start sending funds to it with full assurance that they'll be able to control those funds at any time given they have the correct verification method.
Calculating the sender
address
sender
addressOn the lowest level, the CREATE2
opcode is used to generate a deterministic smart account address through the combination of a fromAddress
, salt
, and initCode
.
const accountAddress = ethers.utils.getCreate2Address(
fromAddress,
salt,
keccak256(initCode)
);
fromAddress
fromAddress
The fromAddress
is the address of the contract that deploys the smart account. We commonly refer to this as the factory.
If the factory address is the same on every chain, we can rely on it to also deploy our smart contract code
on all networks under the same address too.
salt
salt
The salt value allows for variability. It is common to use a default value of 0
. But we can use other values if we want to allow a single verification method method to control multiple addresses. This would be comparable to how a seed phrase can generate many addresses through the derivation path.
initCode
initCode
The initCode
(not to be mistaken with the initCode
on a User Operation), is the code used by the EVM to deploy the smart contract on-chain. This value is hashed with keccak256
and the result used to calculate the CREATE2
address.
Accounts and Factories
Given the details of generating a CREATE2
address, the biggest dependancy on a deterministic address is the factory (i.e. the fromAddress
). If the factory address is not consistent across networks, then our account address won't be either. There are 2 types of factories we can leverage.
1. A generic deterministic deployer
We can leverage a generic CREATE2
factory like the arachnid deterministic deployer. Given a salt
and initCode
, the factory will deploy the account with the same address on all networks. This is possible because the factory itself is deterministically deployed using a pre-signed EOA transaction.
The arachnid deterministic deployer is also the same factory used to deterministically deploy the EntryPoint contract. However, using this factory to deploy the sender
isn't the most efficient method.
A majority of smart accounts are not unique in their implementation. In fact, they almost always follow a proxy pattern. This is when the account is a light storage layer that delegates code execution to a singleton implementation.
By using this approach to deploy the sender
, our initCode
that we send to the factory would need to encode a lot of redundant information such as the proxy setup, implementation address, and initialization call data. This consequently increases the size of our User Operation and users end up paying a higher gas cost than required.
2. A specialized factory for each account implementation
Instead, we can use the generic CREATE2
factory to deploy a specialized factory that can deploy smart accounts in an efficient manner. Since the generic CREATE2
factory is deterministic, this property carries over to the specialized factory and also to the smart account.
Rather than encoding redundant data every time we deploy a new smart account with the same implementation, we can instead use a specialized factory to do all that for us. This has the benefit of reducing the size of our User Operation and lowers the callData
cost.
For a concrete example take a look at these two contracts in the EF's reference implementation:
Consider the following function in SimpleAccountFactory
:
function createAccount(address owner,uint256 salt) public returns (SimpleAccount ret) {
address addr = getAddress(owner, salt);
uint codeSize = addr.code.length;
if (codeSize > 0) {
return SimpleAccount(payable(addr));
}
ret = SimpleAccount(payable(new ERC1967Proxy{salt : bytes32(salt)}(
address(accountImplementation),
abi.encodeCall(SimpleAccount.initialize, (owner))
)));
}
The createAccount
function has encapsulated all the common details needed to create a SimpleAccount
. This includes the ERC1967Proxy
setup, specifying the implementation, and encoding the initialization call data. All we need to do is specify the inputs that makes this account unique. In this case, that's the owner
and a salt
. This makes account deployment simpler and cheaper since our User Operation needs to encode less.
Recap
In this section we discussed:
sender
== user's smart account address.- Why we need it to be deterministic to maintain parity with an EOA address.
- How we can calculate a
sender
address withCREATE2
. - How we rely on factories to deterministically deploy our smart accounts.
- Why it's better to use specialized factories for a given account implementation.
In the next section we will look at how the concept of factories and account deployments work within ERC-4337 through the User Operation initCode
field.
Updated about 1 year ago