As we have shown in our previous post there is a great deal of trust invested in the wasm artifacts when dealing with ink! smart contracts (or substrate runtimes). Here we present a simple tool to analyze those wasm artifacts statically and detect some type of backdoors and anomalies that could be found in wasm.
Goal: understand what is happening in the wasm module
An ink! wasm module must export only 2 functions:
deploy()
call()
deploy()
is used as a constructor and it is called when the deployer instantiates the contract for the first time. call()
is the main entrypoint used to execute all the implemented functionality. The dispatcher reads the input and select which internal method to jump to. As a simple first step, lets take a look at the wasm function call graph for a normal and a backdored flipper contract.
A normal Flipper
Note: It's important to keep in mind that function names may vary from one call graph to another. These function names, such as func1
, func2
, etc are dynamically generated based on the sequence of functions.
A backdoored flipper
Find the differences
From a quick inspection we can design a few static features to make good contracts from bad ones.
All good contracts written using ink! have a single path from call() to input(). Having two different paths to reach input from a call is so far a good way to determine if a contract is backdoored.
There are a few other very simple static checks that while at it we can add to the mix:
- must export only call() and deploy()
- must import only valid host functions
- must import only the lastest versions (seal) of host functions
- If it accepts payment it must have import functions to send it out ....
The tool
Check out our tool to statically check wasm artifacts generated by ink! smart contracts. Please let us know if you think of any other static feature to check or how to improve the ones we have.
We took our simple backdoor and run it through all know simple parity example contracts. Then check if the simple detector idea works. TL;DR it works!
~/inkscope/octopus $ poetry run inkscope --file ./tests/flipper.wasm --check_backdoor
Checking for backdoor
❌ Check failed
There are multiple paths from call to input.
Note that there are 2 paths from
call()
toinput()
. This is very uncommon in regular ink! smart contracts. There is a high chance that this smart contract has been manipulated.
Contract Name | Number of Paths from Call to Input | |
ink-examples/ | Original | Backdoored |
upgradeable-contracts/set-code-hash | 1 ✅ | 2 ❌ |
upgradeable-contracts/delegator | 1 ✅ | 2 ❌ |
upgradeable-contracts/delegator/delegatee | 1 ✅ | 2 ❌ |
incrementer_copy | 1 ✅ | 2 ❌ |
payment-channel | 1 ✅ | 2 ❌ |
trait-erc20 | 1 ✅ | 2 ❌ |
rand-extension | 1 ✅ | 2 ❌ |
multi-contract-caller | 1 ✅ | 2 ❌ |
contract-terminate | 1 ✅ | 2 ❌ |
basic-contract-caller | 1 ✅ | 2 ❌ |
basic-contract-caller/other_contract | 1 ✅ | 2 ❌ |
erc20 | 1 ✅ | 2 ❌ |
flipper | 1 ✅ | 2 ❌ |
multisig | 1 ✅ | 2 ❌ |
custom-allocator | 1 ✅ | 2 ❌ |
vesting | 1 ✅ | 2 ❌ |
contract-transfer | 1 ✅ | 2 ❌ |
psp22-extension | 1 ✅ | 2 ❌ |
e2e-call-runtime | 1 ✅ | 2 ❌ |
trait-flipper | 1 ✅ | 2 ❌ |
incrementer | 1 ✅ | 2 ❌ |
erc721 | 1 ✅ | 2 ❌ |
dns | 1 ✅ | 2 ❌ |
erc1155 | 1 ✅ | 2 ❌ |
The future
We highlight the importance of being able to understand and analyze the low level artifact sent to the blockchain, here wasm. The plan is to extend this tool to extract more human readable features and to make it detect common pitfalls and bugs. We want to build a toolset that enable deployers to check the to the contract or runtime one last time before committing trust to it forever.