LearnWeb3 Logo

15 min read

·

2 years ago

+7,000 XP
805
38
6

Account Abstraction on Ethereum: ERC-4337 Breakdown


In the previous lesson, we provided an overview of Account Abstraction, explaining its concept and purpose. Now, we are ready to delve deeper into how Account Abstraction operates within the context of Ethereum.


So, what are we waiting for? Let's get started!


Introduction

Vitalik first brought up the topic of account abstraction shortly after the launch of Ethereum V1, but no implementation gained consensus at that time.


After a while, ERC-4337 was introduced, providing a proposal for implementing account abstraction without necessitating alterations to the consensus layer. The goal was to incorporate all the functionalities of Account Abstraction we discussed in the previous lesson seamlessly without requiring changes to the core protocol.


Isn't that remarkable? Let's delve into the workings of this proposal to see how it operates exactly.


Implementation







One of the fundamental changes introduced by ERC 4337 to Ethereum is the introduction of a new mempool - the UserOperations mempool. ERC-4337 introduces a pseudo transaction object known as the UserOperation. When users interact with a dApp, their smart contract wallet will send a UserOperation to the UserOperation mempool instead of the typical transaction mempool.


This UserOperation shares some fields similar to a regular transaction, such as sender, to, calldata, maxFeePerGas, maxPriorityFee, signature, and nonce. However, it also includes additional fields, which we will discuss in the following sections.


In this context, Bundlers can be perceived as either block builders themselves, equipped with specific code to integrate the new mempool, or nodes capable of relaying transactions to block builders using services like Flashbots MEV-Relay, or something similar.

These Bundlers actively monitor the UserOperation mempool for UserOperations, and they assemble Bundle Transactions by combining multiple UserOperations. The Bundle Transaction then calls the EntryPoint contract's handleOps function.


An EntryPoint contract is a singleton contract, meaning there is only one instance of it. The primary purpose of this contract is to verify and execute  Bundle Transaction.


It's crucial to note that before incorporating a UserOperation into the Bundle Transaction, Bundlers perform a simulateValidation function call in the EntryPoint contract. If the validation fails, they exclude the UserOperation from the Bundle Transaction.


ERC-4337 User Operations are broadcasted to?

  • The pending transactions mempool

  • Privately shared with a specific Bundler


What is the role of the bundler?

  • To create a bundle transaction

  • To create a user operation

  • None of the above


Non-Paymaster flow



Let's first focus on the handleOps method within the Entry Point contract, specifically in a simple non-paymaster scenario:


handleOps is responsible for the following tasks:

  1. Creating an account if it doesn't exist using the initCode provided in the UserOperation object. We will learn about initCode shortly - but basically it's the initialization code required to set up your wallet for the first time.

  2. Invoking validateUserOp on the account's contract. The account should verify the signature (using whatever algorithm) and process the fee payment. If the account fails the validation for this particular UserOperation, the Entry Point contract may choose to skip this specific UserOperation from the bundle or revert the process entirely.

  3. Ensuring that the fee paid by the account to cover gas is sufficient to cover the maximum possible gas.


Once these initial validations are completed, the method proceeds to execute the UserOperation as follows:


It calls the account with the User Operations calldata. The account is expected to parse the calldata. A suggested flow involves having an execute function that handles the remaining calldata processing.


What is handleOps responsible for doing? Select all that apply.

  • Calls validateUserOp to validate the user operation

  • Creates an account, if it doesn't exist

  • Ensuring the fee paid by the account or paymaster is enough to cover gas


Paymaster





If there is a paymaster involved, then the process changes a bit to add a few additional steps:


In the handleOps method of the EntryPoint contract, there is an additional check to verify whether the paymaster has sufficient ETH deposited inside the EntryPoint contract to cover the cost of the UserOperation. Subsequently, it calls the validatePaymasterUserOp function on the paymaster contract to ensure that the paymaster will indeed pay for the transaction. If successful, the postOp call is made to the Paymaster after the main UserOperation execution is completed.


However, there is a potential exception in this case. Sometimes, the paymaster may decide to pay for gas in exchange for another token - for example, USDC - from the user. After execution is completed in the wallet, the paymaster covers the gas cost, but if the postOp fails due to the user not having enough USDC funds, it could lead to a problem.


To address this situation, a mechanism was devised such that postOp function is called twice.


Initially, it is called by the EntryPoint contract as part of the same call that includes the execution within the wallet. Thus, if postOp reverts, the execution also reverts.

However, it is essential to ensure that the paymaster receives their payment, as they have already agreed to cover the costs during the validatePaymasterUserOp call.


Therefore, EntryPoint callspostOp again. However, at this point, the situation is that we are only at the stage of validatePaymasterOp (because execution was reverted). Fortunately, since this validation succeeded, it means the account has sufficient funds at this stage. As a result, when postOp is called again, it becomes successful.  



How many times is postOp called?

  • Once

  • Twice

  • Thrice


Refer to the diagram above for a clearer understanding of the flow!


UserOperation


Let's dig a little deeper into how the UserOperation object really looks like:


{ // the account making the operation sender: address, // Anti-replay param nonce: uint256, // Needed if the account is not yet created initCode: bytes, // data to pass to sender to execute callData: bytes, // gas to execute the main execution callGasLimit: uint256, // the gas for verification verificationGasLimit: uint256, // bundler does the preverification before creating a bundle transaction // preVerificationGas would cover those costs. preVerificationGas: uint256, // max fee per gas ( read EIP-1559 for more clarification) maxFeePerGas: uint256, // priority fee per gas ( read EIP-1559 for more clarification) maxPriorityFeePerGas: uint256, // address of paymaster and the data to be sent to paymaster paymasterAndData: bytes, // signature of the sender signature: bytes }


It's essential to grasp that ERC-4337 doesn't impose any restrictions on the signature, allowing each wallet to have a different signature scheme. However, to prevent replay attacks across multiple cross chains, it's crucial that the signature depends on the chainId and the EntryPoint contract address.


Another interesting aspect to consider is that the implementation of nonce generation is left to the wallet, and ERC-4337 doesn't impose a particular method. This means accounts have the flexibility to define their own logic for generating and validating nonces.


Regarding paymasterAndData, the first 20 bytes represent the paymaster address, and the remainder holds the data intended for transmission.


Now, let's delve into initCode and its purpose:


We require a way to create a wallet if the account doesn't exist, and initCode facilitates this process.


In the EVM, the CREATE2 opcode comes into play. It becomes useful when we want to receive assets without actively sending a transaction from our wallet ( meaning without deploying it). CREATE2 allows us to do something known as Counterfactual Deployments. These are contract deployments where the address of the to-be-deployed contract can be known beforehand, unlike regular deployments where the address is assigned after the contract is deployed. CREATE2 allows deterministic calculation of the address to which a wallet would be deployed, utilizing factors like a salt, the init code of the contract, and the address of the contract calls CREATE2.


This allows users to set up smart accounts and know their address before the wallet contract is ever deployed. Users can receive funds and assets on that address during that time, and when they want to do something with those assets and send their first transaction from the wallet, the handleOps function will use the initCode to deploy the wallet contract along with making the first transaction.


Within the initCode, the first 20 bytes represent the Factory address, and the remaining bytes hold the Factory data.

A Factory is a contract that calls CREATE2 to create a wallet for the user. Think of it as the main contract responsible for deploying wallet contracts of a specific type. You probably know, for example, that the main Uniswap v2/v3 contract is responsible for deploying new Pool contracts when a new trading pair is added. This is similar, where a Wallet Factory can deploy new Wallet contracts when a user wants one to be deployed.


The handleOps method calls this Factory using the initCode found in the UserOperation. Each factory can be specialized in creating a specific kind of wallet, such as wallets requiring multisig to sign a transaction, etc. Factories will also be subject to the same restrictions as Paymasters, necessitating a stake and imposing restrictions regarding storage to ensure they do not act maliciously, etc


A contract is deployed as soon as the user wishes to create a smart account

  • True

  • False


Account


The account contract deployed by the factory must have the following interface: 


interface IAccount { function validateUserOp(...); }


Here are a few tasks that an account must handle:


  1. It should validate that the execute call originates from a valid EntryPoint contract.

  2. It should then verify the validity of the signature, using whatever mechanism it wants to.

  3. It should cover the missingAccountFunds, which refers to the additional funds required to execute this UserOperation if the account's deposit within the EntryPoint contract is insufficient.


An important point to emphasize is that the validateOp The function does not actually execute the calls listed in the calldata of a UserOperation. Instead, it focuses on validating the signature and ensuring that there are sufficient funds to cover the gas cost. This ensures that the validation remains valid between the validate and execute phases. 


To achieve this, certain restrictions were introduced for the validateOp function:


  1. Certain opcodes like BLOCKHASH, TIMESTAMP, etc., are forbidden, as their values can change between the validation and execution phases.

  2. The wallet only accesses its own associated storage, including its own contract's storage and another contract's storage where the storage slot corresponds to the wallet. The reason for restricting storage access is that storage can be altered between validation and execution.



As mentioned previously, the EntryPoint contract requests the maximum amount of gas, while the remainder is retained with the EntryPoint contract for the wallet to withdraw at a later time. This extra gas also serves as a payment for future operations. As explained before, the account only needs to pay the missingAccountFunds, which the Entry Point contract doesn't have for this particular contract.


Is it possible to have a smart account which only allows transactions if the block hash ends with 4 zeroes?

  • Yes

  • No


Bundlers


Bundlers first perform all the validations and then perform the executions. 


If we do validation and then execution of one operation before validating another operation, the execution of the operation before could have changed the storage needed for the validation of another. As a reason, not only do Bundlers do validations and executions together but also restrict that one Bundle Transaction would only have one UserOperation from a given wallet. This makes sure that we can limit the possibility that the storage would change between certain validations and executions.


Paymasters


Paymaster should have the following interface:


function validatePaymasterUserOp(...) function postOp(...) // add a paymaster stake (must be called by the paymaster) function addStake(...) // unlock the stake (must wait unstakeDelay before can withdraw) function unlockStake() // withdraw the unlocked stake function withdrawStake(...)



Paymasters are required to make a deposit to the EntryPoint contract, which is utilized to cover gas costs. However, they also need to have a locked stake. The purpose of staking is to prevent potential abuse of the system by malicious paymasters, who might initially agree to pay for operations and then reject them, resulting in the. Thereby creating a DoS attack.


Thus we needed a reputation system. In this reputation system, similar to how it works in traditional web environments (web2), a paymaster that causes a significant number of failures is subject to a temporary ban.


Each bundler individually tracks the reputation of paymasters, allowing for customized reputation systems.


It is important to note that even if a paymaster behaves maliciously, their stake is not forfeited, which is distinct from other systems that penalize malicious actors by stashing their stakes.


However, there are a few exceptions to the staking rule:


If a paymaster can succeed in the validation step but fail in the execution step, it might be due to storage changes occurring between the two steps. This could be the result of multiple operations altering the same storage in the paymaster's contract.


If the paymaster doesn't utilize storage at all or only relies on the account's storage and not its own, they might not be required to pay a stake. The reason is that if the validation passes, it is highly unlikely that execution will fail, reducing the chances of the paymaster being malicious.


By introducing these exceptions, the system maintains fairness while accommodating certain scenarios where the traditional staking requirement may not be applicable.


Aggregators


As we all know, Bundlers currently validate UserOperations one by one. However, wouldn't it be convenient if we could validate multiple ops with just one signature?


To address this, EIP-4337 makes use of aggregate signatures, a well-known concept in cryptography. It enables multiple messages signed with different keys to have a unified signature. When verified, it implies that all other signatures were also valid.


The Aggregator should adhere to the following interface:


interface IAggregator { function validateUserOpSignature(...) function aggregateSignatures(...) function validateSignatures(...) }



As a result, aggregators were introduced as smart contracts that implement an aggregation scheme. By having an aggregator, gas consumption is reduced, as signatures require significant cryptographic work.



However, since each wallet has its own signature scheme, it's crucial for each wallet to determine its compatibility with a specific aggregator. Bundlers also need to whitelist supported aggregators. Note that aggregators are subject to staking requirements and may be throttled up or down if they engage in malicious behavior, similar to Paymasters. They have similar exceptions to staking as the Paymasters.



Here's how the changes affect the process:


  1. If an account wants to perform validation using an aggregator, it should return the aggregator's address in the validateUserOp() function. This function is called by the simulateValidation function within the EntryPoint contract from the Bundler.


  2. Instead of directly verifying the signature, the Bundler should call the validateUserOpsSignature function in the aggregator to verify the signature.


  3. The aggregateSignatures method of the aggregator is called by the Bundler to combine the signatures of the UserOperations that specify the same aggregator address.


  4. In case there are operations that require aggregators, the handleOps The function is replaced with handleAggregatedOps in the Entry Point contract. It handles the same logic as handleOps, but it is also called the validateSignatures function in the aggregator to validate the aggregated signature.


This mechanism is particularly useful for roll-ups, as they require significant data compression.


Entry Point Contract


Entry Point contract should have the following interface:


function handleOps(...) function handleAggregatedOps(...) struct UserOpsPerAggregator { UserOperation[] userOps; IAggregator aggregator; bytes signature; } function simulateValidation(...) function getNonce(...) // return the deposit of an account function balanceOf(...) // add to the deposit of the given account function depositTo(...) // withdraw from the deposit function withdrawTo(...)



In addition to all the details we discussed above, it is also essential to ensure that an account is designed to support upgradability. (NOTE: The entry point contract is not upgradeable though) The account should be capable of performing a self-call to update its code address with a new one containing the address of the new entry point contract. To learn more about upgradeable smart contracts, you can visit the senior track in the Ethereum degree program. 🙂


Is the EntryPoint contract in itself upgradeable?

  • Yes

  • No


Wrapping Up


One thing you've probably realized from reading this lesson is that ERC-4337 is a very generic, abstract standard leaving a lot of implementation details up to the different parties involved in the process. Bundlers manage reputation of paymasters, wallet contracts can do basically whatever they want however they want, paymasters can implement payment any way they want to, and so on.


It is natural to want to see more concrete, specific examples of things when reading about such an abstract concept. In the following lessons, we will build projects that utilize Account Abstraction on Ethereum in different ways, which can help provide some concrete examples to these abstractions. As you're going through those lessons, it may be helpful to come back and revisit this one to tie the puzzle pieces together.


Conclusion


Hope you enjoyed this lesson! In the next lesson, we will start building our first project - social login smart accounts using Biconomy's SDK. Hope you are as excited as I am 🎉


If at any point you felt confused or had questions, or just want to say hi, hop in our Discord Server and someone will be there to help you out!

*You must be signed in to submit quiz
You must be signed in to post comments
User avatar

ProtectiveSatin

·

8 months ago

Gn

0
User avatar

Lrazmil

·

9 months ago

Nice

1
User avatar

Ten-year-oldHaymaker

·

11 months ago

good

0
User avatar

farzad.ch

·

11 months ago

nice

0
User avatar

HopelessGramophone

·

11 months ago

Great

0
BUGG Logo