0%
HomeBlogTechnical
Account Abstraction Part 3: Wallet Creation

Account Abstraction Part 3: Wallet Creation


Published on February 14, 20235 min read

Wallet creation

Something we haven’t addressed yet is how each user’s wallet contract ends up on the blockchain in the first place. The “traditional” way to deploy a contract is use an EOA to send a transaction with no recipient that contains the contract’s deployment code. That would be pretty unsatisfying here, because we’ve just done so much work to make it so someone can interact with the chain without an EOA. If a user needed their own EOA to get started, what was it all for?

To be clear about we want, someone who wants a wallet but doesn’t have one yet should be able to end up with a brand new wallet on-chain, either paying for their own gas with ETH (even though they don’t have a wallet yet) or by finding a paymaster who will pay for their gas (which we covered in part 2), and they should be able to do this without ever creating an EOA.

There’s another less obvious goal that’s also pretty important.

When I create a new EOA, I can generate my private key locally and claim my account without sending any transactions.

I can tell people my address and start receiving ETH or tokens before I’ve ever sent a transaction myself.

We would like our wallet to have the same property, meaning that we should be able to tell people our address and receive assets before we’ve actually deployed our wallet contract.

Prerequisite: Deterministic contract addresses with CREATE2

The bit about being able to receive assets at our address before we’ve actually deployed our contract is a hint about how we need to implement this. It implies that although we may not have deployed our wallet contract yet, we need to know what address it will end up at when we finally get around to actually deploying it.

💡An address to which a contract will eventually be deployed but hasn’t yet is called a counterfactual address. Fancy.

The key ingredient to make this happen is the CREATE2 opcode which is designed for exactly this. It deploys a contract at an address that can be deterministically calculated from the following inputs:

  • The address of the contract calling CREATE2

  • A salt, which can be any 32-byte value

  • The init code of the contract being deployed

The init code is a blob of EVM bytecode specifying a function which when executed returns a different blob of EVM bytecode that is saved as the newly deployed smart contract. This is an interesting tidbit many people don’t realize: when you deploy a contract, the code you submit is not the same code that ends up in your contract. In particular, using the same init code multiple times does not guarantee that the deployed contracts will have the same code, because the init code could be reading from storage or using opcodes like TIMESTAMP.

First attempt: Entry point deploys arbitrary contracts

Now that we know about CREATE2, our first plan is simple. We’ll let users pass in init code and have the entry point deploy the contract if it doesn’t exist yet. First we’ll add a new field to user operations:

Copied
struct UserOperation {   // ...   bytes initCode; }

Then, we’ll update the validation part of the entry point’s handleOps to do the following:

As part of validating a user op, if the op has nonempty initCode, then use CREATE2 to deploy a contract with that init code.

Then proceed with the rest of validation as normal:

  • Call the newly created wallet’s validateOp method

  • Then, if the op has a paymaster, call the paymaster’s validatePaymasterOp method

This is a pretty good attempt!

It accomplishes all the goals discussed above: users can deploy arbitrary contracts and know ahead of time the addresses at which they’ll end up, and the deployment can be sponsored by paymasters or by the user themselves (if they deposit ETH into the address where the contract will end up).

But there are a handful of flaws, which all revolve around the fact that we’re asking the user to submit and the entry point to validate an arbitrary blob of bytecode:

  • When a paymaster is looking at a user op, it can’t reasonably analyze a blob of bytecode to decide if it wants to pay for it or not.

  • When the user is submitting a blob of bytecode to deploy a contract, they can’t readily validate that the bytecode they’re submitting does what they want. If the user is using a tool to deploy their contract, then if the tool is malicious or hacked it can submit init code that installs a backdoor into the deployed contract, and this subterfuge cannot easily be detected.

  • Recall from part 1 that the bundler wants to simulate validation for each operation it includes in a bundle so that it doesn’t end up including ops that fail validation that it then has to pay gas for out-of-pocket. But since the init code is arbitrary code, it can very easily succeed during simulation but fail during execution.

We need a way for users to deploy contracts without submitting arbitrary bytecode, and for other participants to be able to have some guarantees about the deployment behavior.

As usual, when we want more execution guarantees, it’s time to introduce a new smart contract.

Better attempt: Introducing factories

Instead of having the entry point accept arbitrary bytecode and call CREATE2, we’ll allow the user to select a contract of their choice to be the one that calls CREATE2. Then these contracts, which we’ll call factories, can specialize for creating different kinds of wallet contracts if they want.

For example, there might be one factory that produces wallets that protect their Carbonated Courage tokens, and another factory which makes wallets that require three out of five keys to sign transactions.

Factories will expose a method which can be called to create a contract:

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

💡We have the factory return the address of the newly created contract so that users can simulate this method to find out what address their contract will have before they deploy it, as was one of our original goals.

We’ll also add fields to a user operation so that if the operation is trying to deploy a wallet, then it specifies which factory to use as well as passing along data that the factory will receive as input:

Copied
struct UserOperation {   // ...   address factory;   bytes factoryData; }
User can call contracts called factories, that specialize in creating different kinds of wallet contracts.
User can call contracts called factories, that specialize in creating different kinds of wallet contracts.

This solves the first two of the issues from the previous section!

  • If a user is calling the factory for wallets that protect Carbonated Courage tokens, then assuming the factory contract is audited, they know for sure that they’ll end up with a wallet that protects Carbonated Courage, doesn’t have backdoors, and they won’t have to review any bytecode to do it.

  • Paymasters can choose to pay for deployments from certain approved factories.

The last issue in the previous section was that deployment code could succeed during simulation but fail during execution.

This is the exact problem we encountered with paymasters’ validatePaymasterOp method, and we’ll solve it in the same way.

Bundlers will restrict factories to only accessing their own associated storage and that of the wallet they’re deploying, and not allow them to call banned methods like TIMESTAMP.

We’ll also ask that factories stake some ETH using the entry point’s addStake method, and then bundlers can throttle or ban factories based on how often their simulations have been falsified recently.

💡As with paymasters, a factory doesn’t need to stake if its deployment method only accesses the associated storage of the wallet it’s deploying, and not the factory’s own associated storage.

And we’ve done it!

Wallet creation has never been better.

At this point, the architecture we’ve created can perform all the functions of the actual ERC-4337!

The only remaining goal that we'll cover in Part 4 about aggregating signatures, enables an optimization to save gas, but doesn’t really add any new functionality.

We can happily stop here and revel in our success, but if we want those gas savings, we can keep going...

You Could Have Invented Account Abstraction Series

Read the other articles in this 4-part series! (Or click here to add Account Abstraction to your app with Alchemy's new Account Kit!)

Section background image

Build blockchain magic with Alchemy

Alchemy combines the most powerful web3 developer products and tools with resources, community and legendary support.