Obfuscating EOSIO smart contracts

Christoph Michel
Coinmonks
6 min readJul 12, 2020

--

There are classes of smart contracts where one would rather not reveal how they work internally. It’s a common theme with private smart contracts that are not intended for other users, for example, trading bot smart contracts because arbitrage trading is a very competitive zero-sum game.

In software development, obfuscation is the deliberate act of creating source or machine code that is difficult for humans to understand. Wikipedia

Usually, this refers to changing the source code to an unreadable mess that is hard to reverse engineer, like writing JavaScript using only []()+! characters. EOSIO smart contracts are compiled to WebAssembly and stored in this format on-chain. Meaning, the obfuscation needs to happen on the WebAssembly level as well. Remember that this only makes it harder for an attacker to reverse engineer the contract but not impossible - given enough time the attacker will still succeed ("Security" by Obscurity).

We are not talking about the cryptographic area of Indistinguishability obfuscation here which can transform a program into a new one that is proven to not reveal anything besides its input-output behaviour. This would actually allow you to include private keys in the program and to implement signature creation in a smart contract but as of 2020, it’s impracticable (even outside of smart contracts).

In this first part we’ll look at the simplest form of obfuscation: Changing the contract’s ABI. This is very simple to implement and already has a big effect as it shows hex values on block explorers forcing someone to dive into the WASM code to decipher action arguments and table structures. Let’s look at some approaches.

1. ABI with obfuscated names

This is the most common approach I’ve seen on-chain. Instead of using meaningful names for the action parameters one simply names them a, b, c or similar. This is also the worst obfuscation because it retains all type information of action parameters and table structures.

I believe the reason why it is still so popular is that it allows easy interaction with the smart contract due to the correct ABI being on-chain and tools like cleos, eosjs or any wallets still work with this approach as they can serialize the actions.

2. No ABI

A different approach is to upload no ABI at all. This results in block explorers not being able to deserialize the data, instead, showing the plain hex data — losing all type information and how many arguments an action has. Let’s test it with this example contract:

We can now set this contract code using the eosio::setcode action but not upload the ABI. Invoking the test action will look like this on eosq:

eosq no ABI
Invoking an action on a contract with no ABI

The action data and table row simply show the plain hex data which is a lot better than the previous approach. But how do we actually call the contract action without the ABI? We need to serialize the action data ourselves. The easiest way to interact with such a contract is by using eosjs and defining a custom AbiProvider.

The AbiProvider is responsible for fetching the ABI for any contract which is then used to serialize the action data upon sending a transaction with api.transact. We can write our own AbiProvider that reads our contract's ABI from the file system and tries to fetch the ABI from the chain for any other contract.

Using this custom AbiProvider in eosjs’ API object is simple:

3. Fake ABI

We can go one step further with confusing anyone looking at the action in block explorers. Instead of uploading no ABI, we create a fake ABI where the action arguments and table structure types don’t match the original ones. We can write the ABI file ourselves or let eosio-cpp do it for us:

We remove the ACTION macro from the original action (and therefore have to implement a custom apply function) and create a fake ACTION with the same action name. Same for the table. This leads to the ABI generator picking up the fake types for the correct action/table names which the block explorers will use to deserialize the data.

In our eosjs’ AbiProvider we still use the correct ABI. The resulting action looks like this on eosq:

eosq with fake ABI
Invoking an action on a contract with a fake ABI

It looks like we send a test action with a single uint8_t id parameter and as if we stored a single boolean stored. The remaining bytes that we used are just not deserialized and not shown at all. It's very easy to trick someone this way unless they check the action traces and look at the actual hex data.

This already works well for protecting your contract from someone trying to make sense by looking at the actions or tables in a block explorer. In the next post, we’ll look at obfuscating WebAssembly code.

Learn EOS Development Signup

--

--

Christoph Michel
Coinmonks

Full Stack Software Engineer #javascript #EOS. Into Recreational Math / CS 🤯 Just message me about anything, my mind is open.