Wisps: The Magical World of Create2
RicMoo — April 30, 2019
Will-o-the-Wisp and Snake ~Herrmann Hendrich (1884)

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

What is initcode?

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:

function initcode() {
return "console.log(\"Hello World\")";
}

This program, when run, will return a "Hello World" program. This enables additional deployment-time configuration, for example:

function init(language) {
if (language == "en_pl") {
return "console.log(\"Ellohay orldway\")";
}
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 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.

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 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:

function init() {
contractBytecode = msg.sender.getPendingBytecode()
return(contractBytecode);
}

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 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.

pragma solidity ^0.5.5;
interface ENS {
function resolver(bytes32) external view returns (address);
}
interface Resolver {
function addr(bytes32) external view returns (address);
}
interface Wisp {
function execute() external;
function die(address owner) external;
}
contract Springboard {
ENS _ens;
bool _mutex;
bytes _bootstrap;
bytes _pendingRuntimeCode;
constructor(address ens, bytes memory bootstrap) public {
_ens = ENS(ens);
_bootstrap = bootstrap;
}
function getBootstrap() public view returns (bytes memory) {
return bootstrap;
}
function _execute(bytes runtimeCode, bytes32 salt) internal {
// Prevent re-entry
require(!_mutex);
_mutex = true;
// Store the desired runtime bytecode
_pendingRuntimeCode = runtimeCode;
bytes memory bootstrap = //bootstrap;
address wisp;
uint256 status;
// Create the Wisp
assembly {
wisp := create2(callvalue, add(bootstrap, 0x20),
mload(bootstrap), salt)
}
// 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 {
_execute(runtimeCode, keccak256(abi.encodePacked(msg.sender)));
}
// 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 {
// Verify the ENS nodehash is owned by msg.sender
Resolver resolver = Resolver(_ens.resolver(nodehash));
address owner = resolver.addr(nodehash);
require(owner == msg.sender);
// Execute based on the nodehash
_execute(runtimeCode, nodehash);
}
// 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) {
return _pendingRuntimeCode;
}
}

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) {
;; let opcodes = [];
;; ASM.split("\n").filter((l) =>
;; (l.substring(0, 1) !== ";" && l.trim() !== "")
;; ).forEach((line) => {
;; line.split(" ").forEach((opcode) => {
;; opcodes.push(opcode);
;; });
;; });
;; 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 {
function deposit() external payable;
function transfer(address, uint) external returns (bool);
function balanceOf(address) external returns (uint);
function withdraw(uint) external;
}
contract Wisp {
function execute() {
// ${Call WETH to convert between ether and WETH}
WETH weth = WETH(0xe7a70dD69D8D2e2f0e775FfAC0C440f23D2ABb72);
WETH(wethContract).deposit(0.1 ether);
weth.withdraw(weth.balanceOf(address(this)) / 2);
// Transfer ether
(0x30bc5920A76B483080523482D7FC2B64c9D5bd80).transfer(1 ether);
}
function die(address addr) {
selfdestruct(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 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

• • •
The Next-Generation Springboard Code (Assembly)

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.

;
; Springboard
;
; The Springboard uses CREATE2 to create stable addresses
; for running bytecode from, while suiciding at the end of
; each invocation, allowing different bytecode to be run
; from the same address in the future
;
; Memory Layout
; * 0x00:0x0c : zeros[0:12]
; * 0x0c:0x20 : msg.sender
; * 0x20: : @bootstrap -- @end
;
; Storage Layout
; * 0xff : Bytecode length and mutex (zero => mutex free)
; * 0x00 : Bytecode word 0
; * 0x20 : Bytecode word 1
; * 0x20 * n : Bytecode word n
;
{! storageScriptLength = 0xff }
@springboard:
; No call data => return the pending Runtime Script
; (usually from the Wisp fetch below)
jumpi(@fetchScript, iszero(calldatasize))
; We use the Runtime Script length as a mutex
; (0: mutex is available)
jumpi(@error, sload({ storageScriptLength }))
; Store the Runtime Script length (acquire the mutex)
sstore({ storageScriptLength }, calldatasize)
; Store the pending Runtime Script
; stack = [ i = 0 ]
0x00
@storeScript:
jumpdest
; storage[i] = calldata[i]
; stack = [ i ]
sstore(dup2, calldataload(dup1))
; stack = [ i + 32 ]
add(0x20, $$)
; if i < calldata.length: keep writing
jumpi(@storeScript, lt(dup2, calldatasize))
; memory[0:32] = msg.sender
mstore(0, caller)
; memory[32:] = @bootstrap .. @end
{! bootstrapLength = end - bootstrap }
codecopy(0x20, { bootstrap }, { bootstrapLength })
; Create2(bootstrap, keccak(caller)) and
; call(callvalue, null) into the Wisp
call(gas, create2(0, 0x20, { bootstrapLength }, sha3(12, 20)),
callvalue, 0x00, 0x20, 0x00, 0x00)
; @TODO: Check the response?
; Clear the runtime bytecode length (free the mutex)
sstore({ storageScriptLength }, 0)
return(0, 0)
; Return the pending Runtime Script
@fetchScript:
jumpdest
; Don't allow the Springboard to receive ether
; (outside of Wisp creation, which is transferred to the Wisp)
jumpi(@error, callvalue)
; stack = [ length, i = 0 ]
sload({ storageScriptLength })
0x00
; Read the next word of the pending Runtime Script
@readScript:
jumpdest
; memory[i:i + 32] = data
mstore(dup2, sload(dup1))
; stack = [ length, i + 32 ]
add(0x20, $$)
; if i < length: keep reading
jumpi(@readScript, lt(dup2, dup2))
; stack = [ length ]
pop return(0, $$); An error has occured
@error:
jumpdest
revert(0, 0);
; Wisp Bootstrap
;
; The Wisp bootstrap fetches the desired Runtime Script bytecode
; from the caller
;
@bootstrap:
; Callback into the caller sans calldata to fetch the
; Runtime Script bytecode
staticcall(gas, caller, 0, 0, 0, 0)
; The length of @preamble code
{! preambleLength = end - preamble }
; Place the full Wisp bytecode length (Preamble + Runtime
; Script onto the stack)
add(returndatasize, { preambleLength })
; memory[32:] = preamble
codecopy(0x20, { rebase(bootstrap, preamble) },
{ preambleLength })
; Fill in the Springboard address in the preamble
{! offset = 0x20 + 21 - 32 }
mstore({ offset }, or(mload({ offset }), caller))
; Append the Runtime Script to the end of the preamble
returndatacopy({ 0x20 + preambleLength }, 0, returndatasize)
; Return preamble + Runtime Script
return(0x20, $$)
; The Preamble is prepended to the Runtime Script
@preamble:
; Place-holder for Springboard address
0x0000000000000000000000000000000000000000
; If we are called by not-the-Springboard, execute the Wisp
jumpi({ rebase(preamble, execute) }, sub(caller, $$))
; Called by the Springboard, so we call ourself (the Wisp)
call(gas, address, 0, 0, 0, 0, 0)
; Return the ether to the Springboard caller (passed
; in by the Springboard)
suicide(calldataload(0))
; Done
return(0, 0)
; Execute a call into the Wisp (except the initial
; Springboard call)
@execute:
jumpdest
; Ourself calling (above), execute the Runtime Script
jumpi({ runtime - preamble }, eq(caller, address))
; If not ourself calling (nor the Springboard), return
; (allows the Wisp to receive ether while on-chain)
return(0, 0)
; The Runtime Script is copied here (after the jumpdest)
@runtime:
jumpdest
@end:
Notes

  • Evanescent ether: we discovered an interesting caveat when self-destructing; if the target of a selfdestruct is 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."