When moving 400+ CryptoKitties and 200+ ENS names from one account to another, it can be a huge inconvenience. The original motivation for this project was to create an Asset Store contract, which would own the tokens (and such), so the they could still be managed, but were bucketed together, allowing easy transfer of all items at once.
The CREATE2 opcode is a new and interesting feature to Ethereum that we've wanted to try out, and see if this idea was possible.
This also allows arbitrary code to be used with the assets, since often, as an asset's contract may be updated over time, new features are enabled or new contracts that can interact with them are released.
What it does
The CREATE2 call uses the byte code of a contract to determine its address, and we were curious if we could get around this restriction, which would allow either:
- An address to be "allocated" in advance, without knowledge of the byte code that would ultimately be placed there
- Re-create multiple different contracts at the same address (after a self-destruct) with wildly different byte code
By using a custom bootstrap (see the ./scripts/deploy-springboard for the assembly), the initCode used by the CREATE2 call, which in turn loads the bytecode from another source (the caller) and installs it at an address.
There are 2 modes it can work from:
- Any EOA (Externally Owned Account), in which case the address has complete control over the contents of the Wisp
- Any ENS Name, in which case the resolved address of the ENS name has complete control over the Wisp, and by changing the "addr" associated with an ENS name, the controlling address of a Wisp can be modified. Basically, it uses ENS as permission control.
How we built it
We built it using ethers.js, and with a lot of experimentation with custom byte code, trial and error (more emphasis on the errors).
The UI currently is a Command-Line Interface, but the library and contract is generic, so it could easily be migrated to any other form.
Challenges we ran into
Challenge #1: Just getting the STATICCALL and byte code to copy properly from the caller. It required a lot of very tiny little steps to figure out where things went wrong, since when they did, everything just failed. Hand-crafting EVM machine code is quite error-prone; when you can't use a compiler, you really realize how wonderful they are.
Challenge #2: Sending ether to a contract that is about to self-destruct completely obliterates it, as it evaporates from existence. We did not plan on that. So we had to fall back onto storing ether in the EOA of the Wisp owner.
Accomplishments that we're proud of
It works!! We are able to create and re-create arbitrary code and the same address repeatedly, and control assets at that address, emit events, send and receive ether and call external functions on any other contract as the Wisp.
What we learned
A lot more about initCode and how the Solidity compiler prepares and writes out the EVM code for constructors.
What's next for Will-o-the-Wisp
Optimizations; the existing bootstrap is very much a proof-of-concept. There are certain errors it should attempt to detect (and throw) and with a little effort the small amount of boiler-plate code required in a Wisp can be entirely eliminated.
We also need to wrap it up in a nicer UI, and will likely include it as part of the Firefly Multi-Sig.
There is also a few other techniques we would like to experiment with for managing ether, rather than moving it back to the controlling EOA, one idea is to "leap-frog" it or "pay-it-forward". The idea is at the end of execution, all ether is moved back into the Springboard; however at the beginning of each user interaction, that user pays the gas for the previous user, to move their ether back into their Wisp. The Wisp can safely hold ether, it just cannot receive it before its self-destruct.