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

On 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

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

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

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:

  1. SimpleAccount.sol
  2. SimpleAccountFactory.sol

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 with CREATE2.
  • 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.