15 min read
·2 years ago
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:
Creating an account if it doesn't exist using the
initCode
provided in the UserOperation object. We will learn aboutinitCode
shortly - but basically it's the initialization code required to set up your wallet for the first time.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 particularUserOperation
, the Entry Point contract may choose to skip this specificUserOperation
from the bundle or revert the process entirely.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 operationCreates 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:
It should validate that the execute call originates from a valid EntryPoint contract.
It should then verify the validity of the signature, using whatever mechanism it wants to.
It should cover the
missingAccountFunds
, which refers to the additional funds required to execute thisUserOperation
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:
Certain opcodes like BLOCKHASH, TIMESTAMP, etc., are forbidden, as their values can change between the validation and execution phases.
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:
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.
Instead of directly verifying the signature, the Bundler should call the
validateUserOpsSignature
function in the aggregator to verify the signature.The
aggregateSignatures
method of the aggregator is called by the Bundler to combine the signatures of the UserOperations that specify the same aggregator address.In case there are operations that require aggregators, the
handleOps
The function is replaced withhandleAggregatedOps
in the Entry Point contract. It handles the same logic ashandleOps
, but it is also called thevalidateSignatures
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!
Continue your learning journey with the following lesson:
Account Abstraction using Biconomy: Social Logins and Paymasters
This is Part 3 in a 4 part series about Account Abstraction. Learn about doing transactions using ERC20 and social logins
ProtectiveSatin
·8 months ago
Gn
Lrazmil
·9 months ago
Nice
Ten-year-oldHaymaker
·11 months ago
good
farzad.ch
·11 months ago
nice
HopelessGramophone
·11 months ago
Great