Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form
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.
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 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.
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:
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:
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:
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.
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:
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:
This solves the first two of the issues from the previous section!
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...
Read the other articles in this 4-part series!