Auditing Smart Contracts with Slither and Echidna

The goal of smart contract audits is to assess code (alongside technical specifications and documentation) and alert project team of potential security issues that need to be addressed to improve security posture, decrease attack surface, and mitigate risk.

An audit helps to detect and resolve security issues before launch, summarized as a set of findings with underlying vulnerabilities, severity, difficulty, sample exploit scenarios, and recommended mitigations.

Given the high cost of smart contract bugs, it’s no surprise that an audit is a key step in the smart contract development lifecycle. However, engaging an auditor can be costly and difficult due to high demand.

In this article, we’ll learn how you can use the open source tools Slither and Echidna to audit Solidity contracts, in order to identify any potential security vulnerabilities.

Thank you for reading.

Note that automated tools cannot replace manual code analysis and review from experienced smart contract security experts. However, they do help you move in the right direction by highlighting important best practices and common security vulnerabilities. An audit remains a critical step that a protocol should undergo before going live.

Utilizing these tools in your development workflow helps you write more secure contracts so fewer security issues remain before the audit phase.

Background

I’m Yos, a Solidity engineer. I recently participated in the Secureum smart contract security audit bootcamp (and ranked in the Top 25 out of 1000 participants.) The 3 month+ program ends with a practical component where the top participants would audit a real protocol, review the code, and raise any security vulnerabilities over the course of a month.

Auditors generate a summarized report for the project team that highlights their findings ranked by severity from high to low, as well as exploit details and recommended mitigations. I applied a combination of manual code analysis based on Solidity best practices and security vulnerabilities alongside automated tools to audit Solidity smart contracts.

The open source tools we’re about to cover in this article (Slither and Echidna) are frequently used by top smart contract auditors to supplement manual code analysis. They help automate the process of reviewing code quality and correctness in an increasingly sophisticated smart contract ecosystem.

In the rest of this article, we’ll first learn about common security pitfalls and how an audit helps, before finally jumping into the tools: Slither and Echidna.

Common Security Pitfalls

Smart contract bugs occur more frequently than it should, with hefty consequences for everyone involved, for both users and project teams.

Common real-world security vulnerabilities include:

  • Inadequate access controls (giving malicious actors access to sensitive operations)
  • Invalid input sanitation (failing to prevent exploits)
  • Incorrect inheritance (leading to other issues)
  • Arithmetic errors (underflow and overflow errors)
  • Business logic errors (failing to match technical specifications)
  • Non-conformance to standards (e.g. ERC20 return values)
  • External interactions with other contracts (e.g. flashloans, ERC777 callbacks, transfer fee ERC20 tokens, etc.)
  • State machine traps leading to locked contracts (e.g. invalid balances reverting require statements)
  • And many others

Even the smallest of smart contract bugs can render the largest of DeFi products useless or open to exploits. This is exacerbated by the fact that most smart contracts are difficult or impossible to upgrade.

How can we mitigate the risk of smart contract bugs and exploits? Can we identify and fix these issues before we launch?

Smart Contract Audits

A smart contract audit is an external security assessment of a project codebase, typically requested and paid-for by the project team. For most projects, the scope is typically the on-chain smart contract code and occassionally any critical off-chain components that interact with the contracts.

The goal of an audit is to assess and alert the project team, typically before launch, of potential security-related issues that need to be addressed to improve security posture, decrease attack surface, and mitigate risk.

An audit helps to detect and resolve security issues (such as the ones highlighted above.) The auditor will share their findings of underlying vulnerabilities, severity, difficulty, sample exploit scenarios, and recommended mitigations. The project team will fix these issues and review them with the auditor before moving forward with the launch.

Here are some examples of audit reports.

Smart Contract Security Tools

Alongside manual code review, smart contract auditing tools automate many of the tasks that can be codified into rules with different levels of coverage, correctness and precision. They enable the auditor to cover more ground and focus on high-level design review.

  • They are fast, cheap, scalable and deterministic compared to manual analysis.
  • They are a snapshot of distilled smart contract security knowledge.
  • Well-suited to detect common security pitfalls and best-practices at the Solidity and EVM level.
  • With manual assistance, they can be programmed to check for application-level, business-logic constraints.

Note that automated tools are a supplement, not a replacement for security expertise and manual code analysis by experienced smart contract developers. Interpreting the outputs of these tools demand some level of familiarity with Solidity.

In the next section, we’ll learn about the following smart contract security auditing tools:

  • Slither, a static analysis tool for Solidity code.
  • Echidna, a fuzz testing tool.

You’ll learn about what they are, what they’re used for, and how you can start using them to audit Solidity smart contracts.

Slither

Slither is a static analysis tool for Solidity code. It takes in one or more contracts and generates a list of security vulnerabilities and other best-practice recommendations.

Static analysis is a technique of analyzing program properties without actually executing the program. This is in contrast to software testing where programs are actually executed with different inputs. Static analysis typically is a combination of control flow and data flow analyses.

Examples of other static analysis tools are ESLint for Javascript and Solhint for Solidity.

Slither performs control-flow and data-flow analyses on smart contracts in the context of their set of detectors which encode common security pitfalls and best-practices. It comes with 70+ built-in detectors covering issues such as uninitialized variables, inheritance, access control, and structural issues. You can also add your own detector functions to look for specific patterns.

Here’s an excerpt of Slither’s built-in detectors:

When you run slither on a contract like so:

slither contracts/strategies/AaveStrategy.sol

Slither will enumerate over each detector and generate a report:

BaseStrategy._swap(uint256[],address[],address).i (contracts/BaseStrategy.sol#312) is a local variable never initialized
UniswapV2Library.getAmountsOut(address,uint256,address[]).i (contracts/libraries/UniswapV2Library.sol#73) is a local variable never initialized
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#uninitialized-local-variables

AaveStrategy._harvest(uint256) (contracts/strategies/AaveStrategy.sol#77-81) ignores return value by aaveLendingPool.withdraw(address(strategyToken),uint256(amountAdded),address(this)) (contracts/strategies/AaveStrategy.sol#80)
AaveStrategy._withdraw(uint256) (contracts/strategies/AaveStrategy.sol#83-85) ignores return value by aaveLendingPool.withdraw(address(strategyToken),amount,address(this)) (contracts/strategies/AaveStrategy.sol#84)
AaveStrategy._exit() (contracts/strategies/AaveStrategy.sol#87-97) ignores return value by aaveLendingPool.withdraw(address(strategyToken),tokenBalance,address(this)) (contracts/strategies/AaveStrategy.sol#92)
AaveStrategy._exit() (contracts/strategies/AaveStrategy.sol#87-97) ignores return value by aaveLendingPool.withdraw(address(strategyToken),available,address(this)) (contracts/strategies/AaveStrategy.sol#95)
AaveStrategy._harvestRewards() (contracts/strategies/AaveStrategy.sol#99-104) ignores return value by incentiveController.claimRewards(rewardTokens,reward,address(this)) (contracts/strategies/AaveStrategy.sol#103)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#unused-return

In each section are the line number of the problematic code as well as reference links to details about the issue:

You can use this information to learn about Solidity best practices and mitigate common security vulnerabilities. However, bear in mind that Slither will generate false positives or non-issues. Determining which issues are relevant requires some familiarity with Solidity and smart contract security.

In addition to general reporting, Slither also comes with a set of printers which helps you inspect your contract’s inheritance tree and variable dependencies.

You can run a printer like so:

slither MyContract.sol --print [printer name]

For example, the vars-and-auth printer gives you a useful summary of modifiers and variable dependencies for your contracts:

Contract Ownable
Contract vars: ['_owner']
Inheritance:: ['Context']

+----------------------------+------------+---------------+----------------+------------+---------------------------------------+----------------+
|          Function          | Visibility |   Modifiers   |      Read      |   Write    |             Internal Calls            | External Calls |
+----------------------------+------------+---------------+----------------+------------+---------------------------------------+----------------+
|        _msgSender()        |  internal  |       []      | ['msg.sender'] |     []     |                   []                  |       []       |
|         _msgData()         |  internal  |       []      |  ['msg.data']  |     []     |                   []                  |       []       |
|       constructor()        |  internal  |       []      |       []       |     []     |      ['_msgSender', '_setOwner']      |       []       |
|          owner()           |   public   |       []      |   ['_owner']   |     []     |                   []                  |       []       |
|    renounceOwnership()     |   public   | ['onlyOwner'] |       []       |     []     |       ['onlyOwner', '_setOwner']      |       []       |
| transferOwnership(address) |   public   | ['onlyOwner'] |       []       |     []     | ['onlyOwner', 'require(bool,string)'] |       []       |
|                            |            |               |                |            |             ['_setOwner']             |                |
|     _setOwner(address)     |  private   |       []      |   ['_owner']   | ['_owner'] |                   []                  |       []       |
+----------------------------+------------+---------------+----------------+------------+---------------------------------------+----------------+

The above is useful for auditors to quickly review if access control modifiers are correctly implemented.

Running the inheritance-graph, cfg, call-graph printers generates visualizations of your contract’s inheritance tree, control flow graph and more. For example, an inheritance tree:

In summary, Slither is a low-cost static analysis tool that you can simply run on your contracts. It helps detect common vulnerabilities and enforce known best practices. Slither Printers are helpful to review a contract’s structure in detail.

Slither analyzes contracts within seconds. However, static analysis often leads to false positives and is not suitable for complex checks against high-level proxies and business logic. For that, we have Echidna.

Echidna

If we model smart contracts as state machines, we want to make sure that no invalid state can be reached. This usually means that there’s inadequate input sanitation or a business logic error somewhere in our code.

Echidna can help us detect invalid state machines. Echidna is a fuzz testing tool for Solidity code.

Fuzz testing is an automated software testing technique that involves providing invalid, unexpected, or random data as inputs to a computer program. The program is then monitored for exceptions such as crashes, failing built-in code assertions, or potential memory leaks.

Echidna work as follows:

  • You start by writing invariants, or high-level properties about your system. For example, an invariant of a stablecoin or DeFi lending protocol may be ‘undercollateralied vaults should always be open for liquidation.’ This is expressed as boolean Solidity expressions like so:

  • Next, Echidna will then call your contracts with a pseudo-random generation of transactions, where it will try to find a sequence of transactions to violate your invariants.

  • Finally, if Echidna manages to break an invariant, it will give you the list of contract calls (state transitions) that it used to get there.

In summary, Echidna is a higher cost, but also higher impact tool compared to Slither.

Echidna requires more time investment than Slither (it takes longer to run, and needs high-level invariants written by humans) but will only produce true positives. The true positives Echidna discovers are usually significant and extreme corner cases missed even by the best manual analyses.

In addition, The act of writing invariants helps ensures key system invariants to be documented and tracked somewhere. As the codebase grows, Echidna tests makes sure that there are no regressions in the contracts’ state machine over time.

Echidna Exercise

Learn by doing and try running Echidna on these sample contracts I’ve written. Each contract has a security vulnerability that needs to be fixed. See if you can identify the underlying issue with Echidna and fix it. Then, rerun Echidna to fuzz test and verify that your fix is correct.

// Sample insecure contract

pragma solidity ^0.7.6;

contract Token {
    mapping(address => uint) public balances;

    function transfer(address to, uint value) public {
        balances[msg.sender] -= value;
        balances[to] += value;
    }
}

contract EchidnaTest is Token {
    address echidna_caller = 0x00a329C0648769a73afAC7F9381e08fb43DBEA70;

    constructor() {
        balances[echidna_caller] = 10000;
    }

    // add the property
    function echidna_test_balance() view public returns(bool){
        return balances[echidna_caller] <= 10000;
    }
}

I recommend checking out Crytic’s guidelines to fully utilize these open source tools.

Summary

Over the course of this article, we learned to:

  • Recognize the impact of smart contract bugs,
  • Review the latest attack vectors and best practices,
  • Use Slither to catch common issues,
  • Use Echidna to test high-level properties,
  • And finally, ship contracts with confidence.

Start with Slither’s built-in detectors to ensure that no simple bugs are present now or will be introduced later. Use Slither to check properties related to inheritance, variable dependencies, and structural issues. As the codebase grows, use Echidna to test more complex properties of the state machine.

Now you too can write more secure contracts by using Slither and Echidna to audit your Solidity code!