Account Abstraction Part 3: Wallet Creation
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 onchain, 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:
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
methodThen, 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:
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:
struct UserOperation {
// ...
address factory;
bytes factoryData;
}
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!)
Related articles
Preparing for the Agave 2.0 Upgrade
ERC-1271 Signature Replay Vulnerability
On October 27th 2023, Alchemy discovered a ERC1271 contract signature replay vulnerability that affected a large number of smart contract accounts (SCA), and led to risks when interacting with several applications.
What is RIP-7212? Precompile for secp256r1 Curve Support
RIP-7212 is a core change in the Ethereum protocol that opens up a way to have cheap, secure, and fast P256 curve verification with a precompiled contract.