The Contract Prelude: A Case Study

0age
Coinmonks
13 min readApr 8, 2019

--

Huh — contract prelude? I’ve never heard that term before…

We’ll get to that, but before jumping into the contract prelude, why it’s useful, and how to use it correctly, allow me to share a quick anecdote about how I first developed an interest in them.

When Constantinople/St. Petersburg went live, I set out to perform the first contract metamorphosis on Mainnet. Now, the goal with metamorphic contracts is to be able to deploy an idiot-proof contract, one that can always be “reset” with new code, regardless of how abominably you goof (because, let’s face it: everybody goofs). With that aim, I put up a metamorphic contract factory and deployed a contract in block 7280025 with a little message, with the intention to SELFDESTRUCT it via a simple CALL and deploy a new one.

However, I quickly realized that I had made an error — SELFDESTRUCT was the first opcode, but I hadn’t placed a forwarding address on the stack first, which meant calling it resulted in a stack underflow and so the contract couldn’t be reset! (Don’t panic: I deployed a new metamorphic contract in block 7280046 with a successful ReInit in block 7280070.)

This highlighted a strange quirk of metamorphic contracts — they suffer from the exact opposite problem of most contracts. Whereas usually the #1 rule is to avoid reaching a SELFDESTRUCT at all costs, the #1 rule for metamorphic contracts (assuming you want them to stay metamorphic) is to ensure that the contract can reach a SELFDESTRUCT, no matter what!

Metamorphic contracts do not fear destruction—they yearn for it!

Shortly thereafter, I stumbled on another big foot-gun (maybe foot-nuke is more appropriate) when working with metamorphic contracts, demonstrated here and outlined thusly: send a wei to a contract, SELFDESTRUCT it, and forward the wei back to itself —and poof! The wei is gone. Not just irretrievable somewhere, but completely annihilated! Puts the concept of “burning ether” in a whole new light.

This is not just an issue that rears it’s head for selfdestruct(address(this)) either — let’s say you create contract A with a wei and contract B with a wei, where A forwards funds to B when destroyed and B forwards funds to A. You might already know that contracts are only scheduled for destruction when SELFDESTRUCT is called, but are actually deleted at the end of the transaction. You might then assume that the account balance is forwarded to the recipient at the end of the transaction as well — in which case, you assume incorrectly!

Funds are actually forwarded by SELFDESTRUCT right away, (and can’t even be refused by the recipient or reacted to as they can be in a CALL,) but once the transaction is over they will be destroyed permanently if it’s been scheduled for a SELFDESTRUCT. So, if you destroy A, then B in the same transaction, you get the following sequence of balances:

{a: 1, b: 1} => {a: 0, b: 2} => {a: 2, b: 0} => {a: 0, b: 0}

I’ll also mention that funds can be rescued from a contract that has already been scheduled for deletion before it has been destroyed— here’s an example. Also, remember that token balances aren’t held by the contract. Instead, they reside in storage of the token contract, so those will persist even after a SELFDESTRUCT, which is nice — as long as you can redeploy, that is!

Real heroes don’t run from the fire: they quench the flames!

Alright, enough anecdoting already — what’s a contract prelude?!

Fair enough, I’ll wrap it up and get to the point.

The Realization

It’s clear that we’ve got the tools we need to redeploy contracts with arbitrary runtime code, but it’s still clearly not idiot-proof.

What we need is a way to ensure that:

  • the contract can always be destroyed, so that it can always be upgraded, and that
  • the funds will be forwarded to a safe destination when it is destroyed, and returned to the contract upon redeployment.

So, we need to ensure that our metamorphic contract can take arms against a sea of troubles and shuffle off this mortal coil. But aye, there’s the rub: the contract needs to destroy itself. There’s no OTHER-DESTRUCT opcode, after all. How do we ensure that the contract can always be destroyed, without constraining the types of contracts we can deploy or expecting them to correctly implement the appropriate destructor every time? By using a contract prelude.

The Contract Prelude

A contract prelude is a snippet of code placed at the very start of a contract’s runtime code. Normally, a Solidity contract will kick off execution by allocating a free memory pointer, reading a function signature from call data, and passing the signature to the function dispatcher, which uses it to jump to the desired function. in contrast, the contract prelude is designed to cut in front of all that, and is executed before the function dispatcher (or anything else, for that matter) when the contract is called.

A prelude can be used to divert contract behavior under certain circumstances without altering the core functionality of the contract they precede — one example that’s been kicked around before is for transparent proxies, where the proxy’s administrator can manage contract upgrades, but any other caller will trigger a DELEGATECALL out to the implementation contract.

Contract preludes can also be used for all kinds of other goodies, like storing metadata or creating contracts that are globally pausable, ownable, non-payable, only-payable, you name it. Another use-case is utilizing the gas provided when calling the contract (or the gas modulo some number) as an additional, cheap source of input — like for an ultra-efficient function dispatcher that picks the jump destination based on the gas used when calling it! An additional, more out-there application is for determining if a contract’s runtime is being executed in the context of a CALL or of a DELEGATECALL (by comparing ADDRESS to a hardcoded contract address) and modifying its behavior based on the given context — for instance, if you know the call is a DELEGATECALL, you can then go Matryoshka doll on it and do another DELEGATECALL without risking an unwanted modification to the contract. And finally, as you’ve probably worked out on your own by now, it’s also very useful for making sure that a contract is destructible.

Say we designate our contract factory address in the contract prelude of each metamorphic contract it deploys so that a CALL from the factory contract to the metamorphic contract will cause the metamorphic contract to SELFDESTRUCT. For any other caller, the prelude will be safely skipped over. Then, we also designate a forwarding address that won’t SELFDESTRUCT and that will return the funds to the contract. Voilà — we now have a “hardened” metamorphic contract. Now, when the factory deploys a new contract, it can immediately check the start of the resultant runtime code using EXTCODECOPY, and if the correct prelude isn’t there, it reverts, undoing the whole thing!

Thus, the idea for Metapod was born.

Nintendo, please don’t sue!

Metapod — Hardened Metamorphic Contracts

Metapod is a metamorphic contract factory that protects against the deployment of non-metamorphic contracts and against lost funds when upgrading (though only if Metapod is the one destroying it via the prelude— there’s only so much any contract can do to protect you against burning your stack if you’re determined enough). The code can be found on Github and deployed contract instances can be found on both Mainnet and Ropsten.

It requires the following 44-byte prelude to be present on all the contracts it deploys:

Metapod (Mainnet) - required prelude (44 bytes):
0x6e2b13cccec913420a21e4d11b2dcd3c3318602b5773 ++
dedicated_vault_address ++
0xff5b
Metapod (Ropsten) - required prelude (44 bytes):
0x6ef647ba29e4dd009d2b7cada21c1c683318602b5773 ++
dedicated_vault_address ++
0xff5b

Here’s a breakdown of the prelude required for the Ropsten instance (note that Metapod uses a compact efficient address with 5 leading zero bytes):

Metapod (Ropsten) - prelude description
pc op opcode [stack]
-- -- --------------------------------------- -------------------
// Push Metapod’s address to the stack (compact so PUSH15 works).
00 6E PUSH15 0xf647ba29e4dd009d2b7cada21c1c68 [metapod_address]

// Push the caller’s address to the stack.
16 33 CALLER [metapod, caller]

// XOR the two — equals zero if they are equal, non-zero otherwise.
16 18 XOR [0 iff metapod]

// Push program counter for the jumpdest at end of prelude to stack.
18 60 PUSH1 0x2b [0 iff metapod, end_prelude_label]

// Jump to end of prelude (pc 43) unless Metapod is the caller.
20 57 JUMPI []

// If Metapod *is* caller, push vault contract’s address to stack.
21 73 PUSH20 dedicated_vault_address [vault_address]

// Schedule account for deletion & forward funds to vault contract.
42 FF SELFDESTRUCT []

// Otherwise we end up here with an empty stack and unused memory.
43 5B JUMPDEST []

The decision to use a dedicated vault contract is motivated by the resultant simplification of our accounting. Rather than storing funds and tracking each account’s respective balance on the factory contract (a task that is complicated significantly by our inability to handle receipt of funds when they are forwarded via SELFDESTRUCT), we instead just isolate each account’s balance at a unique contract that can then send the funds back to the associated metamorphic contract on redeployment.

The derivation of the vault contract’s address is a little more involved, and each salt (20_byte_caller_address ++ 12_byte_identifier => 32_byte_salt) has a unique vault, but you can determine it for a given salt by calling the findVaultContractAddress(bytes32 salt) view function on Metapod — or you can just call getPrelude(bytes32 salt) to get the entire prelude for a given salt in one shot.

Metapod has three state-changing methods:

  • deploy(uint96 identifier, bytes initializationCode) takes the provided contract initialization code (yes, you can use a constructor!) and uses the caller’s address and the provided identifier as the salt when deploying a transient contract via CREATE2. This contract then pulls in the initialization code and deploys the metamorphic contract using CREATE, then is destroyed. Of course, it then checks for the required prelude and only allows the deployment if it’s there. No vault contract is deployed until it’s actually needed.
  • destroy(uint96 identifier) triggers the metamorphic contract associated with the given identifier to SELFDESTRUCT, forwarding any funds to the dedicated vault contract.
  • recover(uint96 identifier) is a dedicated convenience method to clean out the vault, transient contract, and metamorphic contract with the given identifier in one fell swoop and send the funds back to the caller. The metamorphic contract has to be destroyed first, though.

Great! So we grab our contract, stick the necessary prelude at the front of our code, and provide it to deployand it reverts.

I was told there would be idiot-proofing…

The first problem is that we’re placing the prelude at the start of the contract’s initialization code instead of at the start of the contract’s runtime code. But that’s a relatively easy fix: the way that Solidity handles initialization is that it runs all the constructor logic, then performs a CODECOPY to place all the runtime code into memory and returns it. All we need to do, then, is locate the PUSH that specifies the length of the code we’re copying, and increase it by the size of the prelude, then insert the prelude at the start of the runtime code instead of at the start of the initialization code. That doesn’t sound too tough, right?

You’re looking for a sequence like this:

...60xx8060yy6000396000f3fe6080604052...

Also note that the 60’s (PUSH1) might be 61’s (PUSH2). Here’s the breakdown:

op opcode     [stack] & <memory> & *deployed_code*
-- ---------- -----------------------------------------
// push the length of the runtime code onto the stack.
60 PUSH1 xx [..., runtime_length]
// duplicate that value, used later for returning the runtime code.
80 DUP1 [..., runtime_length, runtime_length]
// push the offset of the runtime code (i.e. location in the code).
60 PUSH1 yy [..., runtime_length, runtime_length, code_offset]
// memory offset is zero — put runtime code at start of memory.
60 PUSH1 0x00 [..., runtime_length, runtime_length, code_offset, 0]

// place the runtime code into memory, consuming three stack items.
39 CODECOPY [..., runtime_length] <runtime_code>

// push the return offset (zero) for code to return from memory.
60 PUSH1 0x00 [..., runtime_length, 0] <runtime_code>

// deploy runtime code, using same length as we did in codecopy.
F3 RETURN [...] *runtime_code*
// the subsequent invalid opcode is there for safety, I guess.
FE INVALID
// runtime code begins here - not executed during initialization.
60 PUSH1 0x80
60 PUSH1 0x40
52 MSTORE
...

So we’ll just change the length (xx) by adding the size of the prelude. We don’t need to change the offset (yy) unless PUSH1 needs to be altered to a PUSH2, in which event we’ll also increase it by 1. Finally, we move the prelude so it’s between the INVALID opcode and the current runtime code. Sweet, let’s try it again.

This time, the contract successfully deploys! Great, now let’s try to use it.

And…

Dang it! the contract’s throwing errors when we call it, spewing out Invalid JUMPDESTs all over the place!

Who would’ve thought that poking compiled EVM code would end up stirring up trouble like this?

How come? Well, all of the JUMP instructions in the runtime code are broken. They don’t point to the correct JUMPDEST — instead, they point to the opcode that’s 44 bytes ahead of the intended JUMPDEST, which is in all likelihood not even a JUMPDEST at all, or at best it’s the wrong one. Also, if you have static constants like hex strings and the like, those are all off too, since they use CODECOPY to grab the value and are no longer pointing to the right location.

The good news is that our prelude is still good — thanks, Metapod. We can destroy the contract and try again.

To fix this, we have a more difficult task — we have to figure out which JUMPDEST each JUMP (and JUMPI) points to and what code offset is provided to each CODECOPY. Then, we need to find where each of those values come from and fix them. (Depending on how they’re used, PC opcodes may also need to be modified — luckily for us, Solidity doesn’t generate any of them, and exclusively relies on static PUSHes for setting jump destinations.)

Now, this would obviously be ideally handled at the language-level, so Solidity or Vyper just generates them correctly based on the provided prelude. But there is a hackier way to do it after the fact — we can try to perform jumpdest analysis of our compiled runtime code and alter it post-compilation. So that’s what I set out to do, and so built a proof-of-concept called Kakuna (I know, I just couldn’t resist).

Well, now I’m definitely getting sued… hopefully my pseudonym holds up!

Kakuna — Flip Some Bits & Rewrap Your Runtime

The basic strategy is to play through the code, placing each PUSH value into the stack and tracking where it gets used. If it never gets DUP’d and ends up being consumed as a JUMP or JUMPI destination or CODECOPY offset, then it can be safely incremented by the length of the prelude like we did earlier with the initialization code. When we hit a JUMPI, and assuming the condition is unknown, we fork and try both options.

Sadly, this game plan falls apart pretty quickly. In a nutshell, the number of potential paths through the code quickly gets out of hand. Furthermore, you can end up in situations where you have circular references and other tomfoolery, which can render the code impervious to static analysis. Kakuna can be improved from here by operating on an intermediate representation of the code (rather than on the bytecode itself). Another way to make it more robust would be to use a “trampoline” that stores a mapping from old JUMPDESTs to new ones (ideally via unstructured storage), then inserts a jump destination in front of all of the existing JUMP and JUMPI opcodes that points to the trampoline, which then makes the correct JUMP. Furthermore, EIP 615 proposes altering JUMP semantics to be more in line with the way eWASM does it, which would be great, but it also proposes a pretty significant overhaul of the EVM as it exists today, which means we’ll have to make do with what we’ve got for now.

It’s not a silver bullet, and will almost certainly fail for complex contracts as it’s currently written, but for relatively simple stuff Kakuna can actually pull the damn thing off. It’ll also perform the same initialization code transformation we used above. So, we provide it with our initialization code and the prelude we want to insert, then take the output and give that to Metapod. And — lo and behold — it actually works!

We did it — with only a few contract fatalities!

Now, is this any safer and more idiot-proof than what we had before? Certainly not, but it’s still pretty interesting if you ask me. Code verification goes out the window, and who knows what gnarly bugs might be hidden deep in the transformed code. If compilers add support for inserting a prelude, many of these issues would be alleviated. In the meantime, at least the contracts can be fixed and redeployed (with a wiped contract state, remember) — I’m going to go out on a limb and call that a win.

Even if this particular case study isn’t your cup of tea, I hope it helped demonstrate how using a contract prelude can be used to your benefit, assuming you can navigate the potential pitfalls. To my fellow preluddites, let’s work together to improve the tooling around them! (To that end, work is already underway to use existing solc functionality to more reliably insert a prelude and modify the output — stay tuned!)

I’d like to thank my many co-conspirators and cronies in this endeavor for their assistance and feedback, including Charles Cooper, Raymond Pulver, Lorenz Breidenbach, Stephane Gosselin, Santiago Palladino, Matt Czernik, and Michael Dunworth.

Get Best Software Deals Directly In Your Inbox

--

--

0age
Coinmonks

Head of Protocol Development @ OpenSea (views are my own)