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 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() {

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("

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