How to use Slither to find smart contract bugs
How to use Slither
The aim of this tutorial is to show how to use Slither to automatically find bugs in smart contracts.
- Installation
- Command line usage
- Introduction to static analysis: Brief introduction to static analysis
- API: Python API description
Installation
Slither requires Python >= 3.6. It can be installed through pip or using docker.
Slither through pip:
pip3 install --user slither-analyzer
Slither through docker:
docker pull trailofbits/eth-security-toolboxdocker run -it -v "$PWD":/home/trufflecon trailofbits/eth-security-toolbox
The last command runs eth-security-toolbox in a docker that has access to your current directory. You can change the files from your host, and run the tools on the files from the docker
Inside docker, run:
solc-select 0.5.11cd /home/trufflecon/
Running a script
To run a python script with python 3:
python3 script.py
Command line
Command line versus user-defined scripts. Slither comes with a set of predefined detectors that find many common bugs. Calling Slither from the command line will run all the detectors, no detailed knowledge of static analysis needed:
slither project_paths
In addition to detectors, Slither has code review capabilities through its printers(opens in a new tab) and tools(opens in a new tab).
Use crytic.io(opens in a new tab) to get access to private detectors and GitHub integration.
Static analysis
The capabilities and design of the Slither static analysis framework has been described in blog posts (1(opens in a new tab), 2(opens in a new tab)) and an academic paper(opens in a new tab).
Static analysis exists in different flavors. You most likely realize that compilers like clang(opens in a new tab) and gcc(opens in a new tab) depend on these research techniques, but it also underpins (Infer(opens in a new tab), CodeClimate(opens in a new tab), FindBugs(opens in a new tab) and tools based on formal methods like Frama-C(opens in a new tab) and Polyspace(opens in a new tab).
We won't be exhaustively reviewing static analysis techniques and researcher here. Instead, we'll focus on what is needed to understand how Slither works so you can more effectively use it to find bugs and understand code.
Code representation
In contrast to a dynamic analysis, which reasons about a single execution path, static analysis reasons about all the paths at once. To do so, it relies on a different code representation. The two most common ones are the abstract syntax tree (AST) and the control flow graph (CFG).
Abstract Syntax Trees (AST)
AST are used every time the compiler parses code. It is probably the most basic structure upon which static analysis can be performed.
In a nutshell, an AST is a structured tree where, usually, each leaf contains a variable or a constant and internal nodes are operands or control flow operations. Consider the following code:
1function safeAdd(uint a, uint b) pure internal returns(uint){2 if(a + b <= a){3 revert();4 }5 return a + b;6}Copy
The corresponding AST is shown in:
Slither uses the AST exported by solc.
While simple to build, the AST is a nested structure. At times, this is not the most straightforward to analyze. For example, to identify the operations used by the expression a + b <= a
, you must first analyze <=
and then +
. A common approach is to use the so-called visitor pattern, which navigates through the tree recursively. Slither contains a generic visitor in ExpressionVisitor
(opens in a new tab).
The following code uses ExpressionVisitor
to detect if the expression contains an addition:
1from slither.visitors.expression.expression import ExpressionVisitor2from slither.core.expressions.binary_operation import BinaryOperationType34class HasAddition(ExpressionVisitor):56 def result(self):7 return self._result89 def _post_binary_operation(self, expression):10 if expression.type == BinaryOperationType.ADDITION:11 self._result = True1213visitor = HasAddition(expression) # expression is the expression to be tested14print(f'The expression {expression} has a addition: {visitor.result()}')Show allCopy
Control Flow Graph (CFG)
The second most common code representation is the control flow graph (CFG). As its name suggests, it is a graph-based representation which exposes all the execution paths. Each node contains one or multiple instructions. Edges in the graph represent the control flow operations (if/then/else, loop, etc). The CFG of our previous example is:
The CFG is the representation on top of which most of the analyses are built.
Many other code representations exist. Each representation has advantages and drawbacks according to the analysis you want to perform.
Analysis
The simplest type of analyses you can perform with Slither are syntactic analyses.
Syntax analysis
Slither can navigate through the different components of the code and their representation to find inconsistencies and flaws using a pattern matching-like approach.
For example the following detectors look for syntax-related issues:
State variable shadowing(opens in a new tab): iterates over all the state variables and check if any shadow a variable from an inherited contract (state.py#L51-L62(opens in a new tab))
Incorrect ERC20 interface(opens in a new tab): look for incorrect ERC20 function signatures (incorrect_erc20_interface.py#L34-L55(opens in a new tab))
Semantic analysis
In contrast to syntax analysis, a semantic analysis will go deeper and analyze the “meaning” of the code. This family includes some broad types of analyses. They lead to more powerful and useful results, but are also more complex to write.
Semantic analyses are used for the most advanced vulnerability detections.
Data dependency analysis
A variable variable_a
is said to be data-dependent of variable_b
if there is a path for which the value of variable_a
is influenced by variable_b
.
In the following code, variable_a
is dependent of variable_b
:
1// ...2variable_a = variable_b + 1;Copy
Slither comes with built-in data dependency(opens in a new tab) capabilities, thanks to its intermediate representation (discussed in a later section).
An example of data dependency usage can be found in the dangerous strict equality detector(opens in a new tab). Here Slither will look for strict equality comparison to a dangerous value (incorrect_strict_equality.py#L86-L87(opens in a new tab)), and will inform the user that it should use >=
or <=
rather than ==
, to prevent an attacker to trap the contract. Among other, the detector will consider as dangerous the return value of a call to balanceOf(address)
(incorrect_strict_equality.py#L63-L64(opens in a new tab)), and will use the data dependency engine to track its usage.
Fixed-point computation
If your analysis navigates through the CFG and follows the edges, you are likely to see already visited nodes. For example, if a loop is presented as shown below:
1for(uint i; i < range; ++){2 variable_a += 13}Copy
Your analysis will need to know when to stop. There are two main strategies here: (1) iterate on each node a finite number of times, (2) compute a so-called fixpoint. A fixpoint basically means that analyzing this node does not provide any meaningful information.
An example of fixpoint used can be found in the reentrancy detectors: Slither explores the nodes, and look for externals calls, write and read to storage. Once it has reached a fixpoint (reentrancy.py#L125-L131(opens in a new tab)), it stops the exploration, and analyze the results to see if a reentrancy is present, through different reentrancy patterns (reentrancy_benign.py(opens in a new tab), reentrancy_read_before_write.py(opens in a new tab), reentrancy_eth.py(opens in a new tab)).
Writing analyses using efficient fixed point computation requires a good understanding of how the analysis propagates its information.
Intermediate representation
An intermediate representation (IR) is a language meant to be more amenable to static analysis than the original one. Slither translates Solidity to its own IR: SlithIR(opens in a new tab).
Understanding SlithIR is not necessary if you only want to write basic checks. However, it will come in handy if you plan to write advanced semantic analyses. The SlithIR(opens in a new tab) and SSA(opens in a new tab) printers will help you to understand how the code is translated.
API Basics
Slither has an API that lets you explore basic attributes of the contract and its functions.
To load a codebase:
1from slither import Slither2slither = Slither('/path/to/project')3Copy
Exploring contracts and functions
A Slither
object has:
contracts (list(Contract)
: list of contractscontracts_derived (list(Contract)
: list of contracts that are not inherited by another contract (subset of contracts)get_contract_from_name (str)
: Return a contract from its name
A Contract
object has:
name (str)
: Name of the contractfunctions (list(Function))
: List of functionsmodifiers (list(Modifier))
: List of functionsall_functions_called (list(Function/Modifier))
: List of all the internal functions reachable by the contractinheritance (list(Contract))
: List of inherited contractsget_function_from_signature (str)
: Return a Function from its signatureget_modifier_from_signature (str)
: Return a Modifier from its signatureget_state_variable_from_name (str)
: Return a StateVariable from its name
A Function
or a Modifier
object has:
name (str)
: Name of the functioncontract (contract)
: the contract where the function is declarednodes (list(Node))
: List of the nodes composing the CFG of the function/modifierentry_point (Node)
: Entry point of the CFGvariables_read (list(Variable))
: List of variables readvariables_written (list(Variable))
: List of variables writtenstate_variables_read (list(StateVariable))
: List of state variables read (subset of variables`read)state_variables_written (list(StateVariable))
: List of state variables written (subset of variables`written)
Last edit: @nhsz(opens in a new tab), August 15, 2023