January 22, 2026

Capitalizations Index – B ∞/21M

The Magical World of Create2 – RicMoo

The Magical World of Create2 – RicMoo

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

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

  1. the sending account (which may be itself a Contract)
  2. 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:

  1. the sending account (which, again, may be itself a Contract)
  2. the contract initcode (which will be executed to generate the contract bytecode)
  3. 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:

  1. 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)
  2. The CREATE2 is used to begin initializing the Wisp Contract
  3. During initialization, the Wisp Contract calls back into the Springboard to get the desired runtime bytecode, which is then returned by the initcode
  4. The Wisp Contract’s execute() method is called to run all the desired operations
  5. 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 
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 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

Published at Tue, 30 Apr 2019 15:19:15 +0000

Previous Article

4 Billion Crypto Adopters Will Outnumber Internet Users, Predicts Binance CEO

Next Article

Microsoft Outlook Hackers Stole Crypto Using Victims’ Emails: Report

You might be interested in …