During the ETHCapeTown Hackathon we put together a quick proof-of-concept Contract Wallet, which uses the powerful new Ethereum opcode, CREATE2.
The CREATE2 opcode allows an Ethereum contract to create a new Contract with a little more control over its address than the standard CREATE opcode, which is very useful when you want to counter-factually compute an address with custom permissions for the use of that address.
The goal with a Wisp Contract, is to demonstrate a technique to generate a stable Contract Wallet address, which can be shared with other users for sending tokens and ether to or used to control on-chain assets (such as ENS names), but does not retain any Contract Wallet code on-chain.
This provides a few advantages; firstly any transaction to this Wisp address requires exactly 21,000 gas, but more importantly, it means the Contract Wallet that has access to the tokens, funds and other on-chain assets cannot be hacked, since its code isn't actually anywhere on-chain.
If there turns out to be a bug in the Contract Wallet, it can be updated on the client side before being used again, during which time all the crypto-assets remain safe.
spoiler + tl;dr: use a static bootstrap initcode to fetch the runtime bytecode for CREATE2 to generate repeatable contract addresses
Before diving too deep into this technique, it is important to understand how an Ethereum contract is deployed.
To deploy a contract, a piece of code called initcode is used, which is simply a regular Ethereum program, which executes normally and returns the actual contract bytecode to install. It is a bit meta, but allows for a very powerful deployment system.
You could imagine in JavaScript, that a "Hello World" initcode, might look something like this:
This program, when run, will return a "Hello World" program. This enables additional deployment-time configuration, for example:
The important thing to notice is that it is not the initcode that gets deployed, but rather the the result of the initcode that gets deployed.
Most Ethereum developers have used the CREATE call and may not have even realized it. The bytecode generated by Solidity is actually initcode, which uses CREATE to execute the operations in the constructor, and then return the rest of the contract without the constructor. The code actually deployed on-chain does not contain any of the constructor code at all.
To determine the address of a deployed contract, the standard CREATE uses:
- the sending account (which may be itself a Contract)
- the sending account's current nonce (possibly a Contract's current nonce)
So, any two different senders will generate different contract addresses and any two different transactions from the same account will generate different contract addresses.
To determine the address of a deployed contract, the new CREATE2 uses:
- the sending account (which, again, may be itself a Contract)
- the contract initcode (which will be executed to generate the contract bytecode)
- a custom salt, which is chosen by the developer
So, same as CREATE, different senders will generate different Contract addresses. But also, the same initcode with different salts will generate different addresses and if the initcode is different (which usually represents different contracts), the Contract address will differ.
One other (and useful) note for CREATE2, since there is a bit more control over the parameters used to compute the contract address, is that if a contract self-destructs, that a new contract can possibly be deployed to the same address again in the future. But CREATE2 cannot create a contract at an address if there is already a non-self-destructed contract at that address.
It is easy to control the sender and the salt, so the only thing necessary to implement the goal of a Wisp Contract is to get around the second initcodeparameter to CREATE2; allowing two different contracts to be deployed at different times, but with the same address.
But the initcode is just a program itself that runs to determine the actual contract to deploy. This feature can be exploited in fun and exciting ways.
The entry point to each Wisp Contract will be a Springboard Contract, which will launch and manage all the calls using CREATE2, so the Springboard Contract will always be the sender. For the salt, the hash of the msg.senderwill be used, so any two calls from the same account will always access the same Wisp Contract.
All that is left is a common (static) bootstrap initcode. The initcode runs from the context of the new contract, but before it is created; which means that its msg.sender is actually the Springboard. So the Springboard will save the desired contract bytecode to its own storage and have a public method called getPendingBytecode(), the pseudocode of the bootstrap is then simply:
Since the bootstrap initcode is always the same, the Springboard can control CREATE2 to generate consistent contract addresses, every time, as long as the contract self-destructs, which is enforced, but outside the scope of this article.
That is it, for the most part, there are a few small details omitted and extras added in the source, but the technique works quite well and interested developers can start playing around with it.
Here is a simple diagram and summary to help illustrate the Wisp Contract Life-Cycle:
- A transaction is made to the Springboard (note that a contract could also call the Springboard, in which case the address of that contract would own the Wisp)
- The CREATE2 is used to begin initializing the Wisp Contract
- During initialization, the Wisp Contract calls back into the Springboard to get the desired runtime bytecode, which is then returned by the initcode
- The Wisp Contract's
execute()method is called to run all the desired operations - The Wisp Contract's
die()method is called to destroy the Wisp, so it can be recreated in the future; note: all ether is returned to the Wisp owner, since ether is not safe in a contract that it is scheduled for self-destruction
As a quick example, here is the code used during the Hackathon which was deployed to Ropsten. It is a bit quick and dirty, as Hackathon code usually is, so please do not use it in production.
This version supports both Externally Owned Accounts (EOA) and ENS names. If called with the ENS named version, the owner of an ENS name is used to control a Wisp Contract, which allows the owner of a Wisp (and all the assets it controls) to be transferred by changing the address that an ENS resolves to.
The bootstrap used during the Hackathon was very simple and hand-coded so it could be easily assembled with a few lines of JavaScript in the deployment script. It is not very robust, and should probably check the return status.
There are several example Wisp Contracts in the GitHub repo, but basically any operation can be placed inside the execute() function.
For the purpose of the Hackathon, all balance forwarded to the Wisp is provided as an endowment during CREATE2, so use this.balance instead of msg.value. This could be forwarded to execute() function instead though, which is what the illustration above shows.
Here are some examples of things that can be done inside a Wisp Contract:
The CREATE2 opcode is awesome and versatile. We are still experimenting with it, and trying it in our Multi-Sig Contract Wallet as an Asset Store.
The goal is to have a reliable and flexible Asset Store, which can be used to hold a large number of CryptoKitties, ENS names and various tokens, while remaining easy to migrate from one Multi-Sig instance to another. Since a Wisp Contract can execute arbitrary code against the assets it controls, it has access to functionality that may not even exist at the time the asset is acquired.
It's basically a fancy delegate call.
Thanks for reading! Any feedback and suggestions are always appreciated, and if you want to keep up to date with my random rambling and projects, follow me on Twitter or GitHub.
Note: Most readers do not need to venture below here, but if interested in a slightly more advanced version we're working on, check it out
Most readers will not need to look at this section, and may safely skip it. It is still very experimental and provided as another example of a slightly more powerful implementation.
This version can accept a Runtime Script as data directly to its address, and execute it as well as guarantees that the Wisp self-destructs at the end of its execution. It is important to note however, that this modifies the bytecode (it appends a preamble), so the runtime script must be position independent, using relative jumps rather than absolute jumps.
It also has not been thoroughly tested yet, and may have exploits, off-by-one-errors and so on.
- Evanescent ether: we discovered an interesting caveat when self-destructing; if the target of a
selfdestructis a contract that is itself secheduled for self-destruction, the ether simply vanishes - Long-lived Wisp-ish Contracts: there is also many use-cases for a Wisp Contract which does not self-destruct at the end of its execution. A DAO for example could use a Springboard and be used to vote on and select new contract bytecode to upgrade the DAO contract; only after a successful vote would the DAO be able to self-destruct and be re-initialized with the new bytecode
- Immortal Wisp-ish Contracts: another use-case we are looking at is using a Springboard that would ensure a multi-sig wallet is deployed with the primary owner already set, but with the set of social-recovery addresses able to be filled in at a later date. This would allow the multi-sig to accept ether and tokens in the meantime, while remaining off-chain, and then once the owner wishes to use their ether, could permanently put the wallet on-chain, adding in the social-recovery addresses
"Do not try and bend the spoon, that's impossible. Instead only try to realize the truth; there is no spoon."