The Technical Nitty-Gritty of Account Abstraction

The Technical Nitty-Gritty of Account Abstraction

Introduction

Account abstraction has become a hot topic in the Ethereum community since the release of ERC-4337. It promises to simplify the way we interact with the Ethereum ecosystem, making it easier to integrate the off-chain world into on-chain activity. However, these enhancements to the way we experience web3 introduce new complexities into the transaction lifecycle that are important for both users and developers to understand. In this guide, we will break down the components that make up account abstraction and walk you through the new transaction lifecycle under ERC-4337.


What is Account Abstraction

Account abstraction is a proposal to increase flexibility in the management and behavior of Ethereum accounts. It achieves this by introducing account contracts: special-purpose smart contracts that define and manage a user's Ethereum account (now called a smart account).

To better understand account abstraction, let's first unpack the various terms relevant to the discussion of account abstraction in Ethereum:

Abstraction

  • Abstraction is a term in computer science that roughly means hiding information about a system or application so it can be used with less knowledge of the processes running in the background.

  • In the context of account abstraction in Ethereum, abstraction refers to the process of hiding the complexity of managing and interacting with Ethereum accounts.

Account

  • An account is a user's representation on the blockchain that can send or receive transactions and interact with other on-chain accounts.

  • Ethereum has two types of accounts: Externally Owned Accounts (EOAs) and contract accounts (smart contracts).

Image Source: MetaMask

To gain a deeper understanding of smart accounts and the various applications of account abstraction, you may refer to my previous article on the topic.\


Implications for Users

With account abstraction, you can enjoy programmable access to funds by using a smart contract wallet instead of relying solely on private keys for security. This is possible because your smart account can have custom rules for spending coins and transferring assets.

For example, imagine you have a smart account that is programmed to only allow transactions to a specific set of addresses. This means that even if someone gains access to your private key, they won't be able to send your funds to an unauthorized address. This is a significant improvement over traditional accounts, which rely solely on private keys for security.

From a network-level perspective, account abstraction means the details of account types are invisible to the Ethereum protocol. Every account (including self-custodial accounts) is simply a smart contract, and users are free to determine how individual accounts are managed and operated.

For example, imagine you have a self-custodial account that is managed by a smart contract. This smart contract can be programmed to automatically execute certain actions based on specific conditions. For instance, it could be programmed to automatically transfer funds to a backup account if it detects that your primary account has been compromised.

Image Source: MetaMask

From a user-level perspective, account abstraction means certain technical details about interacting with Ethereum accounts are concealed behind higher-level interfaces. This improves wallet designs and significantly reduces the complexity of using web3 applications.

For example, imagine you are a first-time user of a dApp that requires you to interact with Ethereum accounts. Without account abstraction, you would need to understand the technical details of managing and interacting with Ethereum accounts, such as gas fees and transaction confirmation times. With account abstraction, these technical details are abstracted away, and you can interact with the dApp using a higher-level interface that is more user-friendly.


Architecture

There are several main components to ERC-4337: UserOperation, Bundler, EntryPoint Contract, Account Contract, Account Factory Contract and Paymaster Contract.

Image Source: Messari


UserOperation

UserOperations are pseudo-transaction objects that are used to execute transactions with contract accounts. These are created by the dApp and can be translated into regular transactions by wallets, so dApps' frontends don't need to change anything to support ERC-4337.

here's what a UserOps struct looks like

struct UserOperation {
    address sender;
    uint256 nonce;
    bytes initCode;
    bytes callData;
    uint256 callGasLimit;
    uint256 verificationGasLimit;
    uint256 preVerificationGas;
    uint256 maxFeePerGas;
    uint256 maxPriorityFeePerGas;
    bytes paymasterAndData;
    bytes signature;
}

For example, imagine you are using a dApp that allows you to trade tokens. When you initiate a trade, the dApp creates a UserOperation that specifies the details of the trade, such as the token amounts and the recipient address.

Below is a table that outlines the meaning of each field.

FieldTypeDescription
senderaddressThe address of the smart contract account
nonceuint256Anti-replay protection; also used as the salt for first-time account creation
initCodebytesCode used to deploy the account if not yet on-chain
callDatabytesData that's passed to the sender for execution
callGasLimituint256Gas limit for execution phase
verificationGasLimituint256Gas limit for verification phase
preVerificationGasuint256Gas to compensate the bundler
maxFeePerGasuint256Maximum fee per gas
maxPriorityFeePerGasuint256Maximum priority fee per gas
paymasterAndDatabytesPaymaster Contract address and any extra data required for verification and execution (empty for self-sponsored transaction)
signaturebytesUsed to validate a UserOperation along with the nonce during verification

EntryPoint Contract

The EntryPoint contract is a singleton contract that verifies and executes the bundles of UserOperations sent to it. It is a global entry point that everyone using ERC-4337-compliant smart contract wallets will use to transact on the EVM. This concept is comparable to the single staking deposit contract.

  • EntryPoint is a smart contract that handles the verification and execution logic for transactions.

  • It is responsible for verifying that UserOperations are valid and executing them on the blockchain.

For example, imagine you are a user of a dApp that uses account abstraction. When you initiate a transaction, your smart contract wallet creates a UserOperation that specifies the details of the transaction, such as the recipient address and the number of tokens to send. This UserOperation is then sent to the EntryPoint contract, which verifies that it is valid and executes it on the blockchain.

The use of an EntryPoint simplifies the logic used by smart contract wallets, pushing the more complicated smart contract functions needed to ensure safety to the entry point rather than in the wallet itself.

Here is an example of a contract deployed on Polygon: https://polygonscan.com/address/0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789

Contract Address: 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789

Contract Account

The Contract Account is an end user's account. At a minimum, it needs to check whether or not it will accept a UserOperation during the verification loop.

With externally owned accounts (EOAs), the address is consistent across all EVM networks. As long as the user has access to the private key, they can access the same address on any network. Ideally, we would like to implement the same experience with Contract Accounts.

Users should be able to deterministically know their account address and keep it consistent on every EVM network, irrespective of whether the account has been deployed or not. This means that they can generate an account and start sending funds to it with the knowledge that they'll be able to control those funds at any time, provided they have the correct verification method.

ERC-4337 achieves this by using the CREATE2 opcode through a Singleton Factory - the Account Factory contract. The Account Factory contract is responsible for deploying new Contract Accounts and ensuring that their addresses are deterministic across all EVM networks.

For example, imagine you are a user who wants to create a new Contract Account. You would generate a salt value (most likely 0) and an initCode that specifies the smart contract code and arguments used for initializing the Contract Account. You would then pass these values to the Account Factory contract, which would use the CREATE2 opcode to deploy the Contract Account on-chain. The resulting address would be deterministic across all EVM networks.


Account Factory contract

The Account Factory contract is a Factory contract that is responsible for deploying Smart Accounts using CREATE2 opcode. It ensures that the addresses of Smart Accounts are deterministic across all EVM networks.

When using a wallet for the first time, the initCode field of the UserOperation is used to specify the creation of the smart contract wallet. This is used concurrently with the first actual operation of the wallet (in the same UserOperation).

For example, imagine you are a user who wants to create a new smart contract wallet using account abstraction. You would initiate a transaction that includes a UserOperation with the initCode field specifies the smart contract code and arguments used for initializing the smart contract wallet. This UserOperation would be sent to the Account Factory contract, which would use the CREATE2 opcode to deploy the smart contract wallet on-chain.

Wallet developers need to implement the Account Factory contract to ensure the determinacy of generated addresses. The Account Factory contract exposes a method that can be called to create a contract:

contract Factory {
  function deployContract(bytes data) returns (address);
}

UserOperation Mempool

Instead of going to the traditional public mempool that hosts pending transactions for externally owned accounts (EOAs), UserOperations will instead be sent to the UserOperation mempool - a dedicated, higher-level mempool specifically for UserOperations.

Like the traditional public mempool, the UserOperation mempool is made up of a permissionless P2P network. UserOperation mempool nodes can use logic similar to today’s Ethereum transaction handling logic to determine whether or not to include or forward a UserOperation to its peers.

For example, imagine you are a user who wants to initiate a transaction using a smart contract wallet that uses account abstraction. You would create a UserOperation specifies the details of the transaction, such as the recipient address and the amount of tokens to send. This UserOperation would be sent to the UserOperation mempool, where it would be validated and propagated to other nodes in the network.


Bundler

A Bundler is a node that listens to the UserOperation mempool, bundles multiple UserOperations together, and sends that bundle to the EntryPoint contract for execution.

On-chain, this would look like an externally owned account (EOA) for smart contract transactions, with the execution of UserOperations as the internal transactions. The “from” address will be the Bundler and the “to” address will be the EntryPoint contract.

Bundlers choose which UserOperation objects to include in their bundle transaction based on the fee-prioritization logic used by block builders on Ethereum today.

For example, imagine you are a Bundler who is listening to the UserOperation mempool. You would receive multiple UserOperations from the mempool and would need to decide which ones to include in your bundle transaction. You would prioritize UserOperations with higher fees to ensure that you are compensated for the gas fees you will pay for the bundle transaction.

Because the Bundler acts as the “from” address when getting the bundle transaction on-chain, the Bundler will pay the gas fee for the bundle transaction in ETH. Bundlers are compensated through fees paid as part of all the individual UserOperation executions.

Before accepting a UserOperation, Bundlers will simulate it to verify that the signature is correct and the UserOperation can pay its fees. Because Bundlers pay for the gas of all the UserOperations, they need to avoid including UserOperations that failed validation to avoid absorbing the gas cost of those not able to pay.

For example, imagine you are a Bundler who receives a UserOperation from the UserOperation mempool. You would simulate the UserOperation to ensure that it is valid and can pay its fees. If the UserOperation fails validation, you would not include it in your bundle transaction to avoid paying the gas cost for an invalid transaction.


Aggregator

Aggregators are optional smart contracts that can validate signatures for multiple Contract Accounts.

When Bundlers bundle UserOperations together for execution on-chain, the bundled UserOperations are validated in a single step, rather than validating each signature separately, by an Aggregator - a helper contract trusted by accounts to validate an aggregated signature. Aggregators improve the efficiency and scalability of transaction processing under ERC-4337.

For example, imagine you are a user who wants to initiate a transaction using a smart contract wallet that uses account abstraction. You would create a UserOperation that specifies the details of the transaction, such as the recipient address and the number of tokens to send. This UserOperation would be bundled together with other UserOperations by a Bundler and sent to the EntryPoint contract for execution. If the UserOperation is associated with a Contract Account, the Aggregator would validate the signature for that Contract Account in a single step, rather than validating each signature separately.

Here is an example of how ERC-4337 signature aggregation compresses transactions

Bundlers/Clients can whitelist supported Aggregators.

For example, imagine you are a developer creating a new smart contract wallet that uses account abstraction. You would need to ensure that your wallet supports the whitelists for Aggregators to ensure that your UserOperations are validated efficiently and scalably. This would allow your UserOperations to be bundled together with other UserOperations and validated in a single step by the Aggregator.

Aggregators that access global storage will need to stake*. Staking is a mechanism that requires a user to deposit a certain amount of tokens as collateral to participate in the network. This helps to ensure that users act in the best interest of the network and discourages malicious behavior.

For example, imagine you are a developer creating a new Aggregator that accesses global storage. You would need to stake a certain amount of tokens as collateral to participate in the network. This would help to ensure that you act in the best interest of the network and discourage malicious behavior.


Paymaster

One of the main reasons why the user experience of using externally owned accounts (EOAs) is so difficult is because the wallet owner needs to find a way to get some ETH before they can perform any actions on-chain.

With Paymasters, ERC-4337 allows abstracting gas payments altogether, meaning someone other than the wallet owner can pay for the gas instead.

For example, imagine you are a user who wants to initiate a transaction using a smart contract wallet that uses account abstraction. You would create a UserOperation that specifies the details of the transaction, such as the recipient address and the number of tokens to send. If the UserOperation is associated with a Contract Account, a Paymaster could pay for the gas fees associated with executing the UserOperation on-chain, rather than requiring the wallet owner to pay for the gas fees in ETH.

Here's how a typical Paymaster flow looks like

Gas abstraction offers plenty of benefits, such as:

  • The need to acquire ETH before performing on-chain actions creates a significant friction point for users who are not crypto-savvy. By allowing someone else to pay for the gas fees, users can interact with the blockchain more easily and without needing to understand the intricacies of gas fees and ETH.

  • Users don’t pay Amazon Web Services (AWS) fees for using web2 apps, so paying gas fees for using decentralized apps (dApps) could feel foreign and wrong to them. Paymasters allow dApps to sponsor those fees instead. Alternatively, dApps can instead allow the user to pay for gas in some ERC-20 token other than ETH, for example with USDC.

For example, imagine you are a developer creating a new dApp that uses account abstraction. You would need to ensure that your dApp supports Paymasters to allow users to interact with your dApp without needing to pay for gas fees in ETH. This would make your dApp more accessible and user-friendly, especially for users who are not familiar with the complexities of the blockchain.


Transaction Flow

Image Source: Stackup


Step 1 - Sending UserOperations

When a client receives a UserOperation, the first thing it must do is run some basic sanity checks to ensure the UserOperation is valid and can be successfully executed.

The checks include:

  1. Checking that either the sender is an existing contract (Contract Account) or the initCode is not empty. Both cannot be true. This ensures a new Contract Account will be created or an existing one used.

  2. If initCode is provided, the client parses the first 20 bytes as a factory address and checks if the factory is staked. This is needed in case the simulation indicates the factory needs to be staked. Any factory that accesses the global state must be staked, as detailed in the reputation, throttling and banning section of EIP-4337.

  3. The client checks that the verificationGasLimit is below the maximum limit and the preVerificationGas is high enough to cover serializing the UserOperation plus the overhead gas. This ensures the op can be verified.

  4. The paymaster address (if provided) is checked to ensure it is a valid contract that can pay for the op and is not banned. The paymaster's stake is also checked depending on its storage usage.

  5. The callgas is checked to be at least the cost of a CALL with a non-zero value.

  6. The maxFeePerGas and maxPriorityFeePerGas are above the minimum the client is willing to accept, and high enough to be included in the current block.basefee.

  7. The sender does not already have another UserOperation in the pool, to limit one op per sender per batch. Staked senders are exempt from this rule.

If the UserOperation passes these checks, the client runs a first op simulation.


Step 2 - Stimulating Transactions

The simulation of a UserOperation involves the following steps:

  1. If initCode is present, the client simulates creating a new Contract Account using that initCode. This ensures the initCode is valid and can create an account.

  2. The client calls account.validateUserOp to validate the UserOperation against that Contract Account. This ensures the UserOperation is valid for that account.

  3. If a paymaster is specified, the client calls paymaster.validatePaymasterUserOp to validate that the paymaster can indeed pay for the UserOperation. This ensures the op has a valid funding source.

  4. Either validateUserOp or validatePaymasterUserOp may return "validAfter" and "validUntil" timestamps indicating the time range that the UserOperation is valid on-chain. The simulateValidation call returns this validity range.

  5. A node may drop a UserOperation if it expires too soon, for example, if it wouldn't make it to the next block.

  6. If the ValidationResult includes "sigFail", the client should drop the UserOperation as it is invalid.

  7. The simulateValidation call always reverts with a ValidationResult response. If any other error is returned, the client rejects the UserOperation as invalid.

If the UserOperation passes this simulated validation, it is considered valid and can be added to the mempool and later included in a bundle. The simulation ensures the op has a high likelihood of successful on-chain execution.


Step 3 - Bundling Transactions

  1. Bundlers collect UserOperations from users and bundle them together into a batch. This batching helps optimize transaction throughput.

  2. Before accepting a UserOperation, bundlers should call the simulateValidation function of the entry point contract locally using an RPC method. This simulates validating the UserOperation.

  3. The simulateValidation call serves two purposes:

    • It verifies that the signature on the UserOperation is correct. This ensures the UserOperation was signed by the sender.

    • It verifies that the UserOperation pays sufficient fees. This ensures the UserOperation can cover its execution costs.

  1. If the simulateValidation call succeeds, the bundler knows the UserOperation is valid and can be bundled.
  1. The bundler then submits the entire batch of bundled UserOperations to the mempool.

  2. The mempool then relays the UserOperations to the entry point contract for on-chain execution.


Step 4 - Handling UserOperations

Essentially when a UserOperation is passed to an EntryPoint Contract, it undergoes three phases/Loops:

Aggregator Loop

If aggregators are used, the entry point contract first verifies the signature from the aggregator to ensure it is authorized to submit UserOperations on behalf of users. This ensures the aggregations are valid and come from a trusted source.

The entry point contract then loops through each UserOperation submitted by the aggregator. For each op, it performs the verification and execution steps described below.

Verification Loop

The entry point contract first checks if the sender account exists. If not, it creates a new contract account using the initCode provided in the UserOperation.

The entry point contract then calls validateUserOp on the contract account to validate the UserOperation against that account. This ensures the op is valid for that account.

There are two options for paying the transaction fees:

  1. If no paymaster is specified, the entry point checks if the contract account has enough deposit to cover the fees. If so, it pays the maximum gas price for the transaction using the contract account's deposit.

  2. If a paymaster is present, the entry point validates that the paymaster has enough stake to cover the fees. If so, the op is validated and the paymaster will pay the fees.

Execution Loop

After validation, the entry point contract calls the contract account with the calldata to execute the op. If a paymaster is used, it calls the paymaster first to pay the fees.

Then, if the signature is verified, the contract account calls the execute function to operate.

Here is a Flow Chart on how transactions proceed in an EntryPoint Contract

Image Source: Stackup


Step 5 - Executing Transactions

  1. After validating the UserOperation and ensuring the fees can be paid, the entry point contract calls the contract account with the UserOperation's calldata.

  2. The contract account then parses this calldata, typically by calling an execute function. The execute function parses the calldata as a series of calls that the account should make.

  3. The contract account then makes those calls as specified in the calldata. This allows the contract account to execute multiple operations in a single transaction.

  4. If a paymaster is used, the entry point contract first calls the paymaster to pay the transaction fees before calling the contract account. The paymaster then covers the fees for that transaction.

  5. Finally, if the signature for the UserOperation is verified, the contract account executes the operation by making the specified calls as parsed from the calldata.


Conclusion

In this technical breakdown, we covered the key components of ERC 4771 Account abstraction and the transaction execution flow through the entry point contract. While account abstraction on Ethereum is still a work in progress, ERC 4771 represents significant progress towards trustless, censorship-resistant smart accounts.

Years after the initial concept was introduced, there is still debate on the best implementation for account abstraction. Proposals like EIP-3074 and EIP-5003 that would enable existing EOAs to upgrade to smart accounts require a hard fork, which seems unlikely in the short term given the community's focus on other upgrades.

Image Source: Messari

However, ERC 4771 provides a workable solution that allows for smart accounts today without requiring a hard fork. While not perfect, it is a major step forward and deserves support from the Ethereum community to continue evolving and improving. As smart contract usage grows, account abstraction will become increasingly important for scalability, interoperability and user experience.

"Progress is impossible without change, and those who cannot change their minds cannot change anything." - George Bernard Shaw


Did you find this article valuable?

Support EnvoyOS | Blog by becoming a sponsor. Any amount is appreciated!