Want to become an expert in Account Abstraction: ERC 4337 ?

Manu Kapoor
Coinmonks
14 min readMay 27, 2023

--

Let’s understand it under the hood.

Every article on Account Abstraction starts with EOA and Contract Account.

Is it the right way to understand AA ?

I differ.

I’ve put in enough effort to avoid going too technical (coding part) for you to understand it like a breeze

Hope it helps.

So, using AA, basically, we’re “abstracting” away the EOA-related complexity from the user,

especially the non-native Web3 ones to enable mass adoption.

Isn’t that the whole purpose ?

Precisely, it is.

So, here are the steps we’d follow:

There is nothing called “transaction” for a user.

A user just wants to do some operation through a web3 application.

Let’s just call it UserOperation (UserOp).

Now, what details should this UserOp contain to enable the user’s “operation” on Blockchain ?

The same parameters that we use in eth_sendTransaction()

✅Target address

✅data (bytes)

✅the amount of Wei to send, if any (value)

✅gas

✅user’s signature to authorize the operation / txn.

✅nonce (to avoid the replay attack)

Now, who will execute the operation ?

It cannot be the user himself as it does not own any EOA.

Wait…

What does EOA exactly do to execute a user’s transaction under the hood ?

Precisely, 2 things:

1️⃣Sign (authorize) the transaction using the owner’s Private key

2️⃣Pay the Gas fee

This means there has to be a second entity in the picture that owns another EOA,

And can execute user transactions on the Blockchain network.

Enter Executor

And enters an obvious question:

Why would this so-called Executor send the user’s transaction and pay the fee out of its pocket (wallet / EOA) ?

Definitely not.

The executor will be compensated under the hood.

How ?

After executing the transaction, the user’s Smart Contract Wallet will refund the Gas spent by Executor.

But, wait… didn’t I say that the user does not need to bother about any kind of wallet ?

Yes, the Smart Contract Wallet or an Account Abstracted Wallet (AA Wallet) will be created by itself when the first transaction is executed by Executor,

though its address can be determined beforehand… details in later posts.

The user’s AA Wallet will have some Eth to pay for the Gas spent by the Executor.

But what if a (malicious) user can get its operation executed and,

does not keep enough Eth to compensate for Executor’s Gas ?

So… the Executor should run a simulation to check this precisely before sending the actual transaction.

Hey, but can we truly trust simulations ?

They may run perfectly beforehand but something may not run in real-time as intended.

Well, when can such things happen ?

e.g. a contract’s storage being read in simulation may end up getting changed by the time Executor actually sends the transaction.

Solution:

Enter EntryPoint.

It’s an audited and source-code-verified Smart Contract deployed on the Blockchain.

The executor can trust this Contract instead of trusting the User or the simulation that he runs.

Now, you must be thinking what are the functions of the EntryPoint ?

1️⃣It will check whether the user has enough funds in its AA Wallet to pay for the Gas.

2️⃣Will keep track of the actual gas consumption upon execution of the user’s operation.

3️⃣Refund the Executor’s gas from the user’s AA Wallet.

Fair enough, Executor is now assured to get compensated.

To ensure that a refund does happen, it is mandated that the user’s AA Wallet will deposit some funds beforehand to the EntryPoint.

This makes sure that EntryPoint doesn’t need to run pillar to post to get the funds out of AA Wallet after the operation is executed.

Now, the word “simulation” used so far is doing the “validation” of the UserOp.

Wait…what was that ?

e.g. There has to be some mechanism to ensure that the rightful owner of the AA Wallet is the one that initiated the User-Operation and no one else.

This, along with a few other checks and balances, is checked using simulation (validation).

Hence, the AA Wallet will contain 2 functions:

1️⃣One, to validate() the UserOp — called and ran locally by the Executor first,

And, then the same validate() is run on-chain by the EntryPoint when Executor submits (sends) this validation-passed Operation (transaction) to it.

2️⃣Two, to execute() the validated UserOp — called and ran on-chain by the EntryPoint Contract.

And, if Executor’s validate() fails, it won’t be sent to EntryPoint.

And, the User won’t be charged any Gas for this as it was run locally with some restrictions.

But, if validation() passes but execute() fails in run time, the user will be charged for the Gas to compensate the Executor as it was anyway running on-chain,

irrespective of their success or failure, eventually, during run time.

In the absence of Paymasters (that sponsor gas and enable gasless transactions — covered later), the AA Wallet has to have some Eth to pay for the gas.

AA Wallet will deposit some Eth to the EntryPoint beforehand.

During validation (), if EntryPoint calculates that it will run short of Gas upon execution(), it will ask the AA Wallet to deposit more Eth.

Any extra / unused Eth that remains with EntryPoint after successfully executing() the transaction will be refunded to the AA Wallet.

But, hey… refunding to any AA Wallet (which in turn is a Contract) can be risky.

How ?

A malicious AA Wallet code may get invoked when EntryPoint tried to refund the extra Eth.

e.g. Remember the notorious Re-entrancy attack vector.

Hence, it’s up to the AA Wallet to call a withdraw() method to get the remaining Eth as a refund.

Just a small security measure with otherwise huge repercussions, if ignored.

Hey… amid all this, you may be thinking what is the benefit to the Executor to bear all the pain ?

Enter maxPriorityFeePerGas

The user has the option of adding a “tip” to its operation data to prioritize its execution.

The Executor also has the option to add a similar “tip” while sending the transaction to the EntryPoint that adds it to the Blockchain.

And, Executor does so to pocket a decent difference between the 2 tips.

Thank god, it’s not doing its job for nothing.

If you closely observe, the EntryPoint has nothing to do with the Executor or the AA Wallet. In particular.

It’s deployed on the Blockchain for the Executor to call one of its functions to send User’s operation / transaction to the network.

This means, there is only 1 EntryPoint for countless Executors and Smart Contract Wallets (Users).

Not so simple enough.

To summarize this overwhelming process…

User creates an UserOp ➡️ Goes to Excutor (EOA) ➡️Executor validate() locally ➡️

If passed, submits to EntryPoint ➡️EntryPoint re-validate() UserOp (now a transaction) on-chain➡️

If passed again, executes() the txn. ➡️refunds excess Eth left with it to the User’s AA Wallet.

Now it’s simple enough.

Enter Bundling & Bundler

Sending every transaction to the Blockchain costs massive 21,000 gas.

It does make sense to bundle multiple UserOps into one single transaction by the Executor.

This saves that multifold of massive gas.

And, hence, now the Executor is better re-named to be called a Bundler.

and the process is called Bundling.

Our mental model made till this point has to be revised now to incorporate bundling.

1️⃣Multiple Users “come” to a Bundler to get their Ops run.

2️⃣ Bundler keeps their Ops in a Mempool,

(similar to how transactions are kept in nodes’ mempool before they are selected by a node to build a block.)

3️⃣Bundler will runs the validations() for all the UserOps locally first.

4️⃣All those who passed it, will be submitted to the EntryPoint, and the rest of Ops discarded.

5️⃣EntryPoint will validate() all the Ops one-by-one with their respective sender’s AA Wallets.

6️⃣Those all passed will be executed() finally, one by one.

7️⃣All the AA Wallets should have deposited their respective Gas fee to the EntryPoint beforehand.

8️⃣Post execution, EntryPoint will refund excess Gas to all the respective wallets as it does keep track of all those one-on-one.

Hope, it seems fair enough.

Bundling also allows the Bundlers to bundle the Ops to maximize their profits / earnings:

MEV — Maximal Extractable Value

A picture is 1000 words:

====================

Enter Paymasters

Consider the following 3 scenarios.

There will arise many more though with time.

Scenario # 1️⃣ (gasless for the user):

Suppose you’re a newbie to the blockchain.

Now that you’ve done away with the cumbersome UX of EOA wallets, you still need to have Eth in your AA Wallet before making any txn.

This is another stumbling block to getting you onboard to Web3.

Scenario # 2️⃣ (gasless for the user):

A Web3 application wants to pay for the gas of its new users to boost its adoption.

Scenario # 3️⃣ (NOT gasless, but liberty to the user to pay for gas in a non-native token):

A dApp allows users to pay for the gas in any token of their choice

OR

A fixed ERC-20 token like USDC (e.g. Instadapp’s Avocado wallet) to pay for every txn made on of the supported multi-chains.

Now, you don’t have to onramp and store different native-chain tokens (Eth, MATIC) to pay for transactions on any chain.

In such a case, the user will pay the gas (e.g. in USDC) to someone (that’s Paymaster),

who actually pays for the same gas in non-native currency on-chain (e.g. Eth, MATIC)

Sounds amazing.

Let’s look at some underlying details.

The Paymasters:

In simplest of its definition, it’s a Smart Contract. Period.

It contains the logic of the criterion basis which it’ll decide whether or not to pay for the gas for a specific UserOp.

Hence, UserOp must contain data that specifies which Paymaster will pay for their gas.

The long chain of events in my previous post gets updated now upon the addition of the Paymasters in the picture:

The user creates an UserOp➡️It has the address of Paymasters (a deployed Contract) ➡️

Goes to Excutor (EOA) ➡️Executor validate() locally, both AA Wallet and Paymster Contract ➡️

If passed, submits to EntryPoint ➡️EntryPoint re-validate() UserOp (now a transaction) on-chain➡️EntryPoint also validates whether the Paymaster mentioned in AA Wallet will pay for the User’s gas➡️ If passed again, executes() the txn. ➡️ refunds excess Eth left with it to the Paymster or the User.

Having said that, Paymaster also has to deposit Eth before the EntryPoint for the user’s gasless txn.

Now, imagine a malicious Paymaster that Users add to their UserOp data,

which fails validations() every time when run by the Bundler.

Now, you may think, what’s wrong with that ?

You may say that we can simply stop including those UserOps in the bundle that fails Bundler’s local validation() and proceed with the UserOps that pass it.

This, if let loose, can make a Paymaster do a Denial of Service (DoS) attack and never let the majority of the UserOps ever run.

So, there must be a mechanism to avoid malicious Paymaster to ruin the executions of UserOps.

Enter Paymaster-reputation:

Bundler will have to assign a reputation to the Paymasters to get to know which one is acting more like a malicious actor in the system and penalize it.

But, how will you penalize a Smart Contract ?

Enter Paymaster-staking:

The paymaster is mandated to have staked some Eth already to the EntryPoint contract,

so that it has a Skin-in-the-Game.

This staked eth will act as a deterrent for any Paymaster to act maliciously.

Of course, it will be allowed to un-stake / withdraw the Eth at a later point, if it wants to.

This staking also deters malicious Paymasters to create multiple identities and do a Sybil-Attack.

Now, that we’ve ensured that the Paymasters don’t burn the Bundler, it’s time that we ensure that the Paymaster does get its due after paying for User’s gas fee.

Now, a million-dollar problem:

What if the user does not pay for the Gas to the Paymaster after its Operation gets executed by the Bundler and the Gas got paid for by the Paymaster?

And, the solution goes like this (bear with me):

1️⃣When Bundler and EntryPoint validate() the Paymaster details mentioned in the UserOp by the user,

the Paymaster takes “note” of the Gas amount specified by the User in the UserOp.

If that seems enough, validation() passes.

2️⃣ During execution, EntryPoint keeps track of gas usage and passes that info to the Paymaster.

3️⃣ In case, the gas consumed on-chain exceeds the gas amount mentioned in the UserOP, the entire execution() reverts.

Yes, you read that right.

4️⃣ If the gas consumed on-chain is less than or equal to the gas mentioned in the UserOp,

Nothing to worry about.

5️⃣Either way, the Paymaster will eventually charge the User for executing() the Operation on-chain irrespective of success or revert of the transaction.

So far, so good.

That’s how Paymasters work under the hood while sponsoring gas (including gaslessness) for the user.

A picture is 1000 words:

1 important point to mention here:

In AA, we have separated 2 entities that were otherwise tightly coupled:

1). The account that sends the txn to the network (Bundler).

2). The account that pays for the gas to get the txn sent (user’s AA wallet OR Paymaster).

Earlier, both of these roles were performed by the user itself using its EOA wallet,

due to which it had to go through all the pain.

Due to AA, you can clearly see how separating these 2 roles made things so easier for a non-native web3 user.

That’s how AA solved the puzzle for seamless-user-onboarding.

====================

If you’re wondering all way along, how the heck wallet gets created behind the scenes,

don’t worry, I’ve got you.

Tighten your seat belts, and…

let’s dive into (a bit technical) the wallet-creation process.

A wallet here is a Smart Contract.

Hence, creating an AA wallet is simply creating a Smart Contract on-chain.

Does that mean whenever an AA Wallet gets created for a user, it has to pay for the Gas ?

Won’t that be again friction against Web3’s mass adoption ?

Answer — The user won’t have to pay for the gas to receive any funds in its AA wallet,

mimicking the experience of using a usual EOA.

BUT, it has to pay for the Gas to send a transaction (execute its operation),

once again mimicking the experience of using a usual EOA.

So, if a user only wants to receive funds from others, there’s no need to deploy the AA wallet, hence no Gas.

But, whenever the user wants to send funds, the AA wallet has to be deployed and the Gas has to be paid for, if not sponsored.

Such an address at which a Contract (AA Wallet) has not yet deployed but will eventually be deployed is called a Counterfactual address (only for receiving funds).

We can deterministically get this address using an EVM opcode: CREATE2.

CRATE2 has been conceptualized exactly to get the address of a contract deterministically at which it will be deployed finally.

It takes 3 inputs:

1️⃣Address of the Contract that calls CREATE2.

2️⃣Any 32-byte value called Salt.

3️⃣ Init-code of the contract being deployed.

(Initcode is the Contract-Creation bytecode, different from the Runtime bytecode)

Not getting into much of the technicality of the CREATE2, let’s move ahead.

All this means, there has to be a way for the users to get their AA Wallets deployed without using / interacting with any arbitrary bytecode.

Also, EntryPoint, Paymasters, etc. would want some assurance of the AA Wallet’s deployment behavior to rule out any possibility of malicious acts.

Enter Account Factory:

Simply, it’s a Smart Contract. Period.

An (audited and approved) contract that the user has the liberty to select to get its AA wallet deployed,

and return its address.

The UserOp, originally submitted by the user to the Bundler, would also contain 2 more inputs:

1️⃣ Address of the Factory to use.

2️⃣ Data that the Factory would need to deploy the user’s AA wallet.

Wallets containing different validation logics can be created by such Factory contracts basis users’ varying preferences.

Similar to “Paymaster-Staking”, we have Factory Contracts staking some Eth to the EntryPoint.

This acts as a “reputation” metric to ban such Factories (from deploying users’ AA wallets) that act malicious more often.

To further explore Account Factory, take a look at the one used by “thirdweb.com”

Stay tuned, we will get to it when I’ll share my learnings soon from an AA-based project built using thirdweb’s SDK.

And, that’s it…

we‘re done understanding the wallet creation under the hood.

A picture is 1000 words:

=========================

If all of this sounds too overwhelming and you expect there must be huge Gas implications for executing such logic on-chain,

then let’s look at the last bit of the AA puzzle to understand an important Optimization technique.

Validating UserOps involves checking signatures.

This means, to check each and every UserOp separately, we have to check each and every signature separately.

Too cumbersome.

Too much gas-wasting and, hence, expensive.

An obvious fix seems to be checking one combined signature for several UserOps to save gas.

But, how do we get 1 combined signature in cryptography ?

Enter Aggregate Signature:

Imagine a signature scheme that generates 1 signature for multiple UserOps that are signed with different Private keys / signatures.

When you validate that 1 signature, it automatically implies that all the underlying signatures are valid.

Such a scheme is called Aggregate Signature.

Roll-ups have a practical use case for such a scheme.

Roll-ups “compress” data and Aggregate Signature “compress” signatures.

As AA wallet can have any sort of custom signature validation scheme, the UserOps in a bundle can lead to different such schemes.

An obvious approach can be grouping UserOps with a similar scheme in 1 group.

To represent different aggregation schemes on chain, we need to have a Contact.

Enter Aggregator:

2 main functions of the Aggregator contract are:

1️⃣Aggregate() / combine the signatures of UserOps with the same scheme into 1 group,

And, generate a single signature for all such UserOps.

2️⃣Validate() the aggregated signature for all underlying UserOps.

The AA Wallet contract has the liberty to select the Aggregator depending upon its custom signature scheme.

Bundler just got an additional responsibility:

Now, the bundler will group all the UserOps having the same Aggregator (same signature scheme) into 1 group,

And send it to the Aggregator for aggregation().

Upon aggregating(), Aggregator returns the aggregated signature to the Bundler.

EntryPoint also got an additional responsibility:

Bundler sends the group of Ops and its aggregated signature to the EntryPoint.

EntryPoint goes to the Aggregator and validates() the single signature for the set of UserOps.

The UserOps that do

not get aggregated in any group are still validated() by the EntryPoint one by one as described earlier.

To cover some edge cases, we may require the Aggregator to stake some Eth,

Likewise Paymasters and Account Factory contracts.

A picture is 1000 words:

Phew….

If all this sounds confusing,

I highly recommend re-reading and you’ll master the “under the hood” of Account Abstraction.

As always, I’m open to feedback, criticism (so that I improve),

and, of course, some appreciation if it added to your knowledge.

Peace. Web3.

Credits: David Philipson, Alchemy

--

--