During the we put together a Contract Wallet, which uses the powerful new opcode, .
The CREATE2 opcode allows an 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 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 , 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
What is initcode?
Before diving too deep into this technique, it is important to understand how an contract is deployed.
To deploy a contract, a piece of code called initcode is used, which is simply a regular 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:
function initcode()
This program, when run, will return a “Hello World” program. This enables additional deployment-time configuration, for example:
function init(language)
return "console.log("Hello World")";
}
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.
CREATE vs CREATE2
Most 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.
Putting it All Together
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 initcode parameter 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.sender will 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:
function init()
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.
The Wisp Contract Life-Cycle
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
The Springboard Code (Solidity)
As a quick example, here is the code which was . 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 . 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.
pragma solidity ^0.5.5;
interface ENS
interface Resolver
interface Wisp
contract Springboard
function getBootstrap() public view returns (bytes memory)
function _execute(bytes runtimeCode, bytes32 salt) internal
// Run the Wisp runtime bytecode
Wisp(wisp).execute();
// Remove the Wisp, so it can be re-created in the
// future, with different runtime bytecode
Wisp(wisp).die(msg.sender);
_mutex = false;
}
// Calling this will create the Wisp on-chain, execute the
// runtime code and then remove the Wisp from the blockchain.
function execute(bytes memory runtimeCode) public payable
// This method is the same as execute, except it uses ENS names
// to manage a Wisp. This allows a simple form of ownership
// management. To change the owner of a Wisp, simply update the
// address that the ENS name resolves to, and all the Wisp's
// assets will be able to be managed by that new address instead.
function executeNamed(bytes32 nodehash,
bytes memory runtimeCode) public payable
// This function is called by the Wisp during its initcode
// from the bootstrap, fetching the desired bytecode
function getPendingRuntimeCode() public view returns
(bytes memory runtimeCode)
}
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.
; mstore(0x00, 0x94198df1) (sighash("getPendingRuntimeCode()"))
0x63 0x94198df1
0x60 0x00
0x52
; push 0x03ff (resultLength)
0x61 0x03ff
; push 0x20 (resultOffset)
0x60 0x20
; push 0x04 (argsLength; 4 bytes for the sighash)
0x60 0x04
; push 0x1c (argsOffset; where the 4 byte sighash begins)
0x60 0x1c
; caller (address)
0x33
; gas
0x5a
; staticcall(gas, caller, args, argsLen, result, resultLen)
0xfa
; mload(0x40) (bytecode bytes length)
0x60 0x40
0x51
; push 0x60 (0x20 + 0x20 + 0x20) (bytecode bytes offset);
0x60 0x60
; return (bytecodeOffset, bytecodeLength)
0xf3
;; // Assemble in JavaScript:
;; function assemble(ASM) );
;; });
;; return ethers.utils.hexlify(ethers.utils.concat(opcodes));
;; }
Example Wisp
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:
interface WETH
contract Wisp
function die(address addr)
}
Conclusion
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 , 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 or .
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
Published at Tue, 30 Apr 2019 15:19:15 +0000