Code is Law 2: Solidity CTF Challenge Writeup

Bypassing on-chain contract code verification, again

Oren Yomtov
5 min readJul 5, 2022

Challenge Overview

This challenge is a patched version of the first one: Code is Law 1, so start by reading its overview.

The challenge contract is identical to the first, except for two changes:

  1. The ERC20 approve function has been removed

2. The only requirement to receive a token is for the receiver to have the codehash of OnlyICanHazToken:

Solving The Challenge

Now that the same trick we used last time has been patched, what can we do?

How about we call ChallengeToken.can_i_haz_token() AND ChallengeToken.transfer() to transfer the token, all in the constructor?

That won’t work because during the contract deployment calling ChallengeToken.can_i_haz_token() will revert on the codehash check because during the contract deployment the the contract’s code is empty.

What if we find a way to change the code of a deployed smart contract?

That way we can first deploy the OnlyICanHazToken contract, receive the token, and then change the code to a Withdrawer contract which lets us withdraw the token.

But smart contracts on the blockchain having immutable code is a very basic concept which we base quite a lot of our security assumptions on.

Let’s take your DEX for example (e.g. Uniswap, 1inch, MetaMask Swap). You chose to give it (unlimited?) approval rights to transfer your precious tokens.

But you wouldn’t just give this much power over your digital assets to anyone, right?

First, you audit the contract’s source code to deem it unexploitable and unruggable. Then you make sure it does exactly what it portrays to do, and nothing else.

After auditing the contract’s code, or delegating the work to an auditor, you can give it approval rights to transfer your tokens without trusting anyone.

Given there are no exploitable vulnerabilities in the code, even if the DEX owner chooses to pull the rug and steal your funds, they can’t because their smart contract simply doesn’t allow them to.

Unless… smart contracts are not in fact immutable, and their code can be freely changed post deployment? 😳

To break this implicit assumption on immutable smart contract code — let’s dive into the internals of smart contract deployment.

How is the address of an Ethereum contract computed? Can it be predetermined? Can we deploy a contract to address X, selfdestruct it, and then deploy a different contract to the same address?

Let’s take a look at the Ethereum Yellow Paper for answers:

The address of the new account is defined as being the rightmost 160 bits of the Keccak-256 hash of the RLP encoding of the structure containing only the sender and the account nonce

Or in pseudocode:

new_contract_address = keccak256(rlp_encode([sender, nonce]))[12:]

So the address is the hash of the sender address and nonce.

Every contract creation transaction increases the sender nonce by one, meaning it is impossible to deploy a contract to the same address twice from an EOA.

What about a smart contract using the create opcode?

Smart contract accounts also have nonces, and each call to create increases the nonce by one, so that’s also a dead end.

Enter EIP-1014: Skinny CREATE2:

Adds a new opcode (CREATE2) at 0xf5, which takes 4 stack arguments: endowment, memory_start, memory_length, salt. Behaves identically to CREATE (0xf0), except using keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:] instead of the usual sender-and-nonce-hash as the address where the contract is initialized at.

Now this is getting interesting; maybe we can use the CREATE2 opcode to deploy two different contracts into the same address, because the address is computed with no relation to the nonce.

The address argument stays the same, the salt is totally under our control, and so is the init_code.

This does allow us to deploy a contract, selfdestruct it, and then deploy the same contract again to the same address.

It’s a neat primitive (coined as “Zombie Contracts” by Jason), but useless for our needs.

Once we change the init_code, the contract address changes.

What now?

Going back to the way smart contracts are deployed, the init_code is not actually the deployed bytecode. What it does is run the constructor, and return the deployed bytecode.

What we can do, is craft a special-purpose init_code that calls an external contract to dynamically retrieve the bytecode and return it.

Bingo! This way, we have a static init_code, which can deploy contracts with any arbitrary deployed bytecode of our choosing.

Back to solving the challenge. Luckily for us, 0age wrote a wonderful factory for redeployable contracts using create2: Metamorphic.

We can use it to execute on our original plan:

What if we find a way to change the code of a deployed smart contract?

That way we can first deploy the OnlyICanHazToken contract, receive the token, and then change the code to a Withdrawer contract which lets us withdraw the token.

You can find the challenge on GitHub, and the full solution in the solution branch.

As part of your audit process, you can use CertiK’s CREATE2 Audit Tool to check whether a contract’s code is immutable or not.

Special thanks to 0age and Jason for writing the medium articles that taught me about the security implications of EIP-1014 (CREATE2), and inspired me to write this challenge to help spread awareness for this issue within the security community.

--

--