Merge branch 'dev' into dev-import-aliasing

pull/2166/head
Feist Josselin 1 year ago committed by GitHub
commit 792f3f5171
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/black.yml
  2. 4
      .github/workflows/ci.yml
  3. 10
      .github/workflows/docker.yml
  4. 2
      .github/workflows/docs.yml
  5. 2
      .github/workflows/doctor.yml
  6. 2
      .github/workflows/linter.yml
  7. 2
      .github/workflows/pip-audit.yml
  8. 4
      .github/workflows/publish.yml
  9. 2
      .github/workflows/pylint.yml
  10. 22
      .github/workflows/test.yml
  11. 64
      CITATION.cff
  12. 137
      README.md
  13. 2
      scripts/ci_test_printers.sh
  14. 5
      setup.py
  15. 8
      slither/__main__.py
  16. 38
      slither/core/compilation_unit.py
  17. 36
      slither/core/declarations/contract.py
  18. 31
      slither/core/declarations/function.py
  19. 33
      slither/core/declarations/solidity_variables.py
  20. 1
      slither/core/expressions/__init__.py
  21. 2
      slither/core/expressions/binary_operation.py
  22. 37
      slither/core/expressions/call_expression.py
  23. 1
      slither/core/expressions/identifier.py
  24. 6
      slither/core/expressions/self_identifier.py
  25. 6
      slither/core/scope/scope.py
  26. 6
      slither/core/variables/__init__.py
  27. 4
      slither/core/variables/local_variable.py
  28. 3
      slither/core/variables/variable.py
  29. 24
      slither/detectors/abstract_detector.py
  30. 5
      slither/detectors/all_detectors.py
  31. 91
      slither/detectors/assembly/incorrect_return.py
  32. 68
      slither/detectors/assembly/return_instead_of_leave.py
  33. 2
      slither/detectors/attributes/incorrect_solc.py
  34. 21
      slither/detectors/naming_convention/naming_convention.py
  35. 5
      slither/detectors/operations/cache_array_length.py
  36. 93
      slither/detectors/operations/incorrect_exp.py
  37. 2
      slither/detectors/statements/deprecated_calls.py
  38. 8
      slither/detectors/statements/divide_before_multiply.py
  39. 24
      slither/detectors/statements/mapping_deletion.py
  40. 123
      slither/detectors/statements/return_bomb.py
  41. 69
      slither/detectors/statements/tautological_compare.py
  42. 3
      slither/printers/all_printers.py
  43. 23
      slither/printers/guidance/echidna.py
  44. 58
      slither/printers/summary/ck.py
  45. 49
      slither/printers/summary/halstead.py
  46. 32
      slither/printers/summary/martin.py
  47. 34
      slither/slither.py
  48. 121
      slither/slithir/convert.py
  49. 18
      slither/slithir/operations/call.py
  50. 9
      slither/slithir/operations/high_level_call.py
  51. 2
      slither/slithir/operations/init_array.py
  52. 10
      slither/slithir/operations/internal_call.py
  53. 2
      slither/slithir/operations/new_array.py
  54. 13
      slither/slithir/operations/new_contract.py
  55. 11
      slither/slithir/operations/new_structure.py
  56. 5
      slither/slithir/operations/type_conversion.py
  57. 5
      slither/slithir/operations/unary.py
  58. 19
      slither/slithir/tmp_operations/tmp_call.py
  59. 15
      slither/slithir/utils/ssa.py
  60. 2
      slither/slithir/variables/constant.py
  61. 10
      slither/solc_parsing/declarations/contract.py
  62. 9
      slither/solc_parsing/declarations/custom_error.py
  63. 2
      slither/solc_parsing/declarations/using_for_top_level.py
  64. 5
      slither/solc_parsing/expressions/expression_parsing.py
  65. 41
      slither/solc_parsing/expressions/find_variable.py
  66. 28
      slither/solc_parsing/slither_compilation_unit_solc.py
  67. 30
      slither/solc_parsing/solidity_types/type_parsing.py
  68. 348
      slither/utils/ck.py
  69. 202
      slither/utils/encoding.py
  70. 5
      slither/utils/expression_manipulations.py
  71. 233
      slither/utils/halstead.py
  72. 157
      slither/utils/martin.py
  73. 40
      slither/utils/myprettytable.py
  74. 197
      slither/utils/upgradeability.py
  75. 96
      slither/visitors/slithir/expression_to_slithir.py
  76. 0
      slither/vyper_parsing/__init__.py
  77. 0
      slither/vyper_parsing/ast/__init__.py
  78. 466
      slither/vyper_parsing/ast/ast.py
  79. 262
      slither/vyper_parsing/ast/types.py
  80. 0
      slither/vyper_parsing/cfg/__init__.py
  81. 66
      slither/vyper_parsing/cfg/node.py
  82. 0
      slither/vyper_parsing/declarations/__init__.py
  83. 524
      slither/vyper_parsing/declarations/contract.py
  84. 39
      slither/vyper_parsing/declarations/event.py
  85. 563
      slither/vyper_parsing/declarations/function.py
  86. 33
      slither/vyper_parsing/declarations/struct.py
  87. 0
      slither/vyper_parsing/expressions/__init__.py
  88. 464
      slither/vyper_parsing/expressions/expression_parsing.py
  89. 150
      slither/vyper_parsing/expressions/find_variable.py
  90. 99
      slither/vyper_parsing/type_parsing.py
  91. 0
      slither/vyper_parsing/variables/__init__.py
  92. 24
      slither/vyper_parsing/variables/event_variable.py
  93. 34
      slither/vyper_parsing/variables/local_variable.py
  94. 29
      slither/vyper_parsing/variables/state_variable.py
  95. 17
      slither/vyper_parsing/variables/structure_variable.py
  96. 80
      slither/vyper_parsing/vyper_compilation_unit.py
  97. 22
      tests/conftest.py
  98. 5
      tests/e2e/compilation/test_data/test_contract_data/test_contract_data.sol
  99. 17
      tests/e2e/compilation/test_resolution.py
  100. 20
      tests/e2e/detectors/snapshots/detectors__detector_CacheArrayLength_0_8_17_CacheArrayLength_sol__0.txt
  101. Some files were not shown because too many files have changed in this diff Show More

@ -26,7 +26,7 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
# Full git history is needed to get a proper list of changed files within `super-linter`
fetch-depth: 0

@ -53,7 +53,7 @@ jobs:
- os: windows-2022
type: truffle
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v4
with:
@ -67,7 +67,7 @@ jobs:
- name: Set up nix
if: matrix.type == 'dapp'
uses: cachix/install-nix-action@v22
uses: cachix/install-nix-action@v23
- name: Set up cachix
if: matrix.type == 'dapp'

@ -17,13 +17,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
id: buildx
with:
install: true
@ -40,14 +40,14 @@ jobs:
type=edge
- name: GitHub Container Registry Login
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker Build and Push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7
target: final

@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v3
- uses: actions/setup-python@v4

@ -29,7 +29,7 @@ jobs:
- os: windows-2022
python: 3.8
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v4

@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
# Full git history is needed to get a proper list of changed files within `super-linter`
fetch-depth: 0

@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Python
uses: actions/setup-python@v4

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
@ -47,7 +47,7 @@ jobs:
uses: pypa/gh-action-pypi-publish@v1.8.10
- name: sign
uses: sigstore/gh-action-sigstore-python@v2.0.1
uses: sigstore/gh-action-sigstore-python@v2.1.0
with:
inputs: ./dist/*.tar.gz ./dist/*.whl
release-signing-artifacts: true

@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
# Full git history is needed to get a proper list of changed files within `super-linter`
fetch-depth: 0

@ -27,7 +27,7 @@ jobs:
type: ["unit", "integration", "tool"]
python: ${{ (github.event_name == 'pull_request' && fromJSON('["3.8", "3.11"]')) || fromJSON('["3.8", "3.9", "3.10", "3.11"]') }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v4
with:
@ -57,7 +57,23 @@ jobs:
npm install hardhat
popd || exit
fi
- name: Install Vyper
run: |
INSTALLDIR="$RUNNER_TEMP/vyper-install"
if [[ "$RUNNER_OS" = "Windows" ]]; then
URL="https://github.com/vyperlang/vyper/releases/download/v0.3.7/vyper.0.3.7+commit.6020b8bb.windows.exe"
FILENAME="vyper.exe"
elif [[ "$RUNNER_OS" = "Linux" ]]; then
URL="https://github.com/vyperlang/vyper/releases/download/v0.3.7/vyper.0.3.7+commit.6020b8bb.linux"
FILENAME="vyper"
else
echo "Unknown OS"
exit 1
fi
mkdir -p "$INSTALLDIR"
curl "$URL" -o "$INSTALLDIR/$FILENAME" -L
chmod 755 "$INSTALLDIR/$FILENAME"
echo "$INSTALLDIR" >> "$GITHUB_PATH"
- name: Run ${{ matrix.type }} tests
env:
TEST_TYPE: ${{ matrix.type }}
@ -84,7 +100,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python 3.8
uses: actions/setup-python@v4
with:

@ -0,0 +1,64 @@
cff-version: 1.2.0
title: Slither Analyzer
message: >-
If you use this software, please cite it using the
metadata from this file.
type: software
authors:
- given-names: Josselin
family-names: Feist
- given-names: Gustavo
family-names: Grieco
- given-names: Alex
family-names: Groce
identifiers:
- type: doi
value: 10.48550/arXiv.1908.09878
description: arXiv.1908.09878
- type: url
value: 'https://arxiv.org/abs/1908.09878'
description: arxiv
- type: doi
value: 10.1109/wetseb.2019.00008
repository-code: 'https://github.com/crytic/slither'
url: 'https://www.trailofbits.com/'
repository-artifact: 'https://github.com/crytic/slither/releases'
abstract: >-
Slither is a static analysis framework designed to provide
rich information about Ethereum smart contracts.
It works by converting Solidity smart contracts into an
intermediate representation called SlithIR.
SlithIR uses Static Single Assignment (SSA) form and a
reduced instruction set to ease implementation of analyses
while preserving semantic information that would be lost
in transforming Solidity to bytecode.
Slither allows for the application of commonly used
program analysis techniques like dataflow and taint
tracking.
Our framework has four main use cases:
(1) automated detection of vulnerabilities,
(2) automated detection of code optimization
opportunities,
(3) improvement of the user's understanding of the
contracts, and
(4) assistance with code review.
keywords:
- Ethereum
- Static Analysis
- Smart contracts
- EVM
- bug detection
- Software Engineering
license: AGPL-3.0-only
commit: 3d4f934d3228f072b7df2c5e7252c64df4601bc8
version: 0.9.5
date-released: '2023-06-28'

@ -1,42 +1,57 @@
# Slither, the Solidity source analyzer
# [Slither, the Solidity source analyzer](https://crytic.github.io/slither/slither.html)
<img src="https://raw.githubusercontent.com/crytic/slither/master/logo.png" alt="Logo" width="500"/>
<img src="https://raw.githubusercontent.com/crytic/slither/master/logo.png" alt="Slither Static Analysis Framework Logo" width="500" />
[![Build Status](https://img.shields.io/github/actions/workflow/status/crytic/slither/ci.yml?branch=master)](https://github.com/crytic/slither/actions?query=workflow%3ACI)
[![Slack Status](https://empireslacking.herokuapp.com/badge.svg)](https://empireslacking.herokuapp.com)
[![PyPI version](https://badge.fury.io/py/slither-analyzer.svg)](https://badge.fury.io/py/slither-analyzer)
Slither is a Solidity static analysis framework written in Python3. It runs a suite of vulnerability detectors, prints visual information about contract details, and provides an API to easily write custom analyses. Slither enables developers to find vulnerabilities, enhance their code comprehension, and quickly prototype custom analyses.
- [Features](#features)
- [Usage](#usage)
- [How to Install](#how-to-install)
- [Detectors](#detectors)
- [Printers](#printers)
- [Tools](#tools)
- [API Documentation](#api-documentation)
- [Getting Help](#getting-help)
- [FAQ](#faq)
- [Publications](#publications)
![PyPI](https://img.shields.io/pypi/v/slither-analyzer?logo=python&logoColor=white&label=slither-analyzer)
[![Slither - Read the Docs](https://img.shields.io/badge/Slither-Read_the_Docs-2ea44f)](https://crytic.github.io/slither/slither.html)
[![Slither - Wiki](https://img.shields.io/badge/Slither-Wiki-2ea44f)](https://github.com/crytic/slither/wiki/SlithIR)
> Join the Empire Hacking Slack
>
> [![Slack Status](https://slack.empirehacking.nyc/badge.svg)](https://slack.empirehacking.nyc/)
> > <sub><i>- Discussions and Support </i></sub>
**Slither** is a Solidity static analysis framework written in Python3. It runs a suite of vulnerability detectors, prints visual information about contract details, and provides an API to easily write custom analyses. Slither enables developers to find vulnerabilities, enhance their code comprehension, and quickly prototype custom analyses.
* [Features](#features)
* [Usage](#usage)
* [How to install](#how-to-install)
* [Using Pip](#using-pip)
* [Using Git](#using-git)
* [Using Docker](#using-docker)
* [Integration](#integration)
* [Detectors](#detectors)
* [Printers](#printers)
* [Quick Review Printers](#quick-review-printers)
* [In-Depth Review Printers](#in-depth-review-printers)
* [Tools](#tools)
* [API Documentation](#api-documentation)
* [Getting Help](#getting-help)
* [FAQ](#faq)
* [License](#license)
* [Publications](#publications)
* [Trail of Bits publication](#trail-of-bits-publication)
* [External publications](#external-publications)
## Features
- Detects vulnerable Solidity code with low false positives (see the list of [trophies](./trophies.md))
- Identifies where the error condition occurs in the source code
- Easily integrates into continuous integration and Hardhat/Foundry builds
- Built-in 'printers' quickly report crucial contract information
- Detector API to write custom analyses in Python
- Ability to analyze contracts written with Solidity >= 0.4
- Intermediate representation ([SlithIR](https://github.com/trailofbits/slither/wiki/SlithIR)) enables simple, high-precision analyses
- Correctly parses 99.9% of all public Solidity code
- Average execution time of less than 1 second per contract
- Integrates with Github's code scanning in [CI](https://github.com/marketplace/actions/slither-action)
* Detects vulnerable Solidity code with low false positives (see the list of [trophies](./trophies.md))
* Identifies where the error condition occurs in the source code
* Easily integrates into continuous integration and Hardhat/Foundry builds
* Built-in 'printers' quickly report crucial contract information
* Detector API to write custom analyses in Python
* Ability to analyze contracts written with Solidity >= 0.4
* Intermediate representation ([SlithIR](https://github.com/trailofbits/slither/wiki/SlithIR)) enables simple, high-precision analyses
* Correctly parses 99.9% of all public Solidity code
* Average execution time of less than 1 second per contract
* Integrates with Github's code scanning in [CI](https://github.com/marketplace/actions/slither-action)
## Usage
Run Slither on a Hardhat/Foundry/Dapp/Brownie application:
```bash
```console
slither .
```
@ -44,18 +59,19 @@ This is the preferred option if your project has dependencies as Slither relies
However, you can run Slither on a single file that does not import dependencies:
```bash
```console
slither tests/uninitialized.sol
```
## How to install
Slither requires Python 3.8+.
> **Note** <br />
> Slither requires Python 3.8+.
If you're **not** going to use one of the [supported compilation frameworks](https://github.com/crytic/crytic-compile), you need [solc](https://github.com/ethereum/solidity/), the Solidity compiler; we recommend using [solc-select](https://github.com/crytic/solc-select) to conveniently switch between solc versions.
### Using Pip
```bash
```console
pip3 install slither-analyzer
```
@ -84,9 +100,9 @@ docker run -it -v /home/share:/share trailofbits/eth-security-toolbox
### Integration
- For GitHub action integration, use [slither-action](https://github.com/marketplace/actions/slither-action).
- To generate a Markdown report, use `slither [target] --checklist`.
- To generate a Markdown with GitHub source code highlighting, use `slither [target] --checklist --markdown-root https://github.com/ORG/REPO/blob/COMMIT/` (replace `ORG`, `REPO`, `COMMIT`)
* For GitHub action integration, use [slither-action](https://github.com/marketplace/actions/slither-action).
* To generate a Markdown report, use `slither [target] --checklist`.
* To generate a Markdown with GitHub source code highlighting, use `slither [target] --checklist --markdown-root https://github.com/ORG/REPO/blob/COMMIT/` (replace `ORG`, `REPO`, `COMMIT`)
## Detectors
@ -182,23 +198,24 @@ Num | Detector | What it Detects | Impact | Confidence
For more information, see
- The [Detector Documentation](https://github.com/crytic/slither/wiki/Detector-Documentation) for details on each detector
- The [Detection Selection](https://github.com/crytic/slither/wiki/Usage#detector-selection) to run only selected detectors. By default, all the detectors are run.
- The [Triage Mode](https://github.com/crytic/slither/wiki/Usage#triage-mode) to filter individual results
* The [Detector Documentation](https://github.com/crytic/slither/wiki/Detector-Documentation) for details on each detector
* The [Detection Selection](https://github.com/crytic/slither/wiki/Usage#detector-selection) to run only selected detectors. By default, all the detectors are run.
* The [Triage Mode](https://github.com/crytic/slither/wiki/Usage#triage-mode) to filter individual results
## Printers
### Quick Review Printers
- `human-summary`: [Print a human-readable summary of the contracts](https://github.com/trailofbits/slither/wiki/Printer-documentation#human-summary)
- `inheritance-graph`: [Export the inheritance graph of each contract to a dot file](https://github.com/trailofbits/slither/wiki/Printer-documentation#inheritance-graph)
- `contract-summary`: [Print a summary of the contracts](https://github.com/trailofbits/slither/wiki/Printer-documentation#contract-summary)
- `loc`: [Count the total number lines of code (LOC), source lines of code (SLOC), and comment lines of code (CLOC) found in source files (SRC), dependencies (DEP), and test files (TEST).](https://github.com/trailofbits/slither/wiki/Printer-documentation#loc)
* `human-summary`: [Print a human-readable summary of the contracts](https://github.com/trailofbits/slither/wiki/Printer-documentation#human-summary)
* `inheritance-graph`: [Export the inheritance graph of each contract to a dot file](https://github.com/trailofbits/slither/wiki/Printer-documentation#inheritance-graph)
* `contract-summary`: [Print a summary of the contracts](https://github.com/trailofbits/slither/wiki/Printer-documentation#contract-summary)
* `loc`: [Count the total number lines of code (LOC), source lines of code (SLOC), and comment lines of code (CLOC) found in source files (SRC), dependencies (DEP), and test files (TEST).](https://github.com/trailofbits/slither/wiki/Printer-documentation#loc)
### In-Depth Review Printers
- `call-graph`: [Export the call-graph of the contracts to a dot file](https://github.com/trailofbits/slither/wiki/Printer-documentation#call-graph)
- `cfg`: [Export the CFG of each functions](https://github.com/trailofbits/slither/wiki/Printer-documentation#cfg)
- `function-summary`: [Print a summary of the functions](https://github.com/trailofbits/slither/wiki/Printer-documentation#function-summary)
- `vars-and-auth`: [Print the state variables written and the authorization of the functions](https://github.com/crytic/slither/wiki/Printer-documentation#variables-written-and-authorization)
- `not-pausable`: [Print functions that do not use `whenNotPaused` modifier](https://github.com/trailofbits/slither/wiki/Printer-documentation#when-not-paused).
* `call-graph`: [Export the call-graph of the contracts to a dot file](https://github.com/trailofbits/slither/wiki/Printer-documentation#call-graph)
* `cfg`: [Export the CFG of each functions](https://github.com/trailofbits/slither/wiki/Printer-documentation#cfg)
* `function-summary`: [Print a summary of the functions](https://github.com/trailofbits/slither/wiki/Printer-documentation#function-summary)
* `vars-and-auth`: [Print the state variables written and the authorization of the functions](https://github.com/crytic/slither/wiki/Printer-documentation#variables-written-and-authorization)
* `not-pausable`: [Print functions that do not use `whenNotPaused` modifier](https://github.com/trailofbits/slither/wiki/Printer-documentation#when-not-paused).
To run a printer, use `--print` and a comma-separated list of printers.
@ -206,13 +223,13 @@ See the [Printer documentation](https://github.com/crytic/slither/wiki/Printer-d
## Tools
- `slither-check-upgradeability`: [Review `delegatecall`-based upgradeability](https://github.com/crytic/slither/wiki/Upgradeability-Checks)
- `slither-prop`: [Automatic unit test and property generation](https://github.com/crytic/slither/wiki/Property-generation)
- `slither-flat`: [Flatten a codebase](https://github.com/crytic/slither/wiki/Contract-Flattening)
- `slither-check-erc`: [Check the ERC's conformance](https://github.com/crytic/slither/wiki/ERC-Conformance)
- `slither-format`: [Automatic patch generation](https://github.com/crytic/slither/wiki/Slither-format)
- `slither-read-storage`: [Read storage values from contracts](./slither/tools/read_storage/README.md)
- `slither-interface`: [Generate an interface for a contract](./slither/tools/interface/README.md)
* `slither-check-upgradeability`: [Review `delegatecall`-based upgradeability](https://github.com/crytic/slither/wiki/Upgradeability-Checks)
* `slither-prop`: [Automatic unit test and property generation](https://github.com/crytic/slither/wiki/Property-generation)
* `slither-flat`: [Flatten a codebase](https://github.com/crytic/slither/wiki/Contract-Flattening)
* `slither-check-erc`: [Check the ERC's conformance](https://github.com/crytic/slither/wiki/ERC-Conformance)
* `slither-format`: [Automatic patch generation](https://github.com/crytic/slither/wiki/Slither-format)
* `slither-read-storage`: [Read storage values from contracts](./slither/tools/read_storage/README.md)
* `slither-interface`: [Generate an interface for a contract](./slither/tools/interface/README.md)
See the [Tool documentation](https://github.com/crytic/slither/wiki/Tool-Documentation) for additional tools.
@ -226,23 +243,23 @@ Documentation on Slither's internals is available [here](https://crytic.github.i
Feel free to stop by our [Slack channel](https://empireslacking.herokuapp.com) (#ethereum) for help using or extending Slither.
- The [Printer documentation](https://github.com/trailofbits/slither/wiki/Printer-documentation) describes the information Slither is capable of visualizing for each contract.
* The [Printer documentation](https://github.com/trailofbits/slither/wiki/Printer-documentation) describes the information Slither is capable of visualizing for each contract.
- The [Detector documentation](https://github.com/trailofbits/slither/wiki/Adding-a-new-detector) describes how to write a new vulnerability analyses.
* The [Detector documentation](https://github.com/trailofbits/slither/wiki/Adding-a-new-detector) describes how to write a new vulnerability analyses.
- The [API documentation](https://github.com/crytic/slither/wiki/Python-API) describes the methods and objects available for custom analyses.
* The [API documentation](https://github.com/crytic/slither/wiki/Python-API) describes the methods and objects available for custom analyses.
- The [SlithIR documentation](https://github.com/trailofbits/slither/wiki/SlithIR) describes the SlithIR intermediate representation.
* The [SlithIR documentation](https://github.com/trailofbits/slither/wiki/SlithIR) describes the SlithIR intermediate representation.
## FAQ
How do I exclude mocks or tests?
- View our documentation on [path filtering](https://github.com/crytic/slither/wiki/Usage#path-filtering).
* View our documentation on [path filtering](https://github.com/crytic/slither/wiki/Usage#path-filtering).
How do I fix "unknown file" or compilation issues?
- Because slither requires the solc AST, it must have all dependencies available.
* Because slither requires the solc AST, it must have all dependencies available.
If a contract has dependencies, `slither contract.sol` will fail.
Instead, use `slither .` in the parent directory of `contracts/` (you should see `contracts/` when you run `ls`).
If you have a `node_modules/` folder, it must be in the same directory as `contracts/`. To verify that this issue is related to slither,
@ -257,7 +274,7 @@ Slither is licensed and distributed under the AGPLv3 license. [Contact us](mailt
### Trail of Bits publication
- [Slither: A Static Analysis Framework For Smart Contracts](https://arxiv.org/abs/1908.09878), Josselin Feist, Gustavo Grieco, Alex Groce - WETSEB '19
* [Slither: A Static Analysis Framework For Smart Contracts](https://arxiv.org/abs/1908.09878), Josselin Feist, Gustavo Grieco, Alex Groce - WETSEB '19
### External publications

@ -5,7 +5,7 @@
cd tests/e2e/solc_parsing/test_data/compile/ || exit
# Do not test the evm printer,as it needs a refactoring
ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,function-id,function-summary,modifiers,call-graph,human-summary,inheritance,inheritance-graph,loc,slithir,slithir-ssa,vars-and-auth,require,variable-order,declaration"
ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,function-id,function-summary,modifiers,call-graph,halstead,human-summary,inheritance,inheritance-graph,loc,martin,slithir,slithir-ssa,vars-and-auth,require,variable-order,declaration,ck"
# Only test 0.5.17 to limit test time
for file in *0.5.17-compact.zip; do

@ -8,7 +8,7 @@ setup(
description="Slither is a Solidity static analysis framework written in Python 3.",
url="https://github.com/crytic/slither",
author="Trail of Bits",
version="0.9.3",
version="0.9.6",
packages=find_packages(),
python_requires=">=3.8",
install_requires=[
@ -16,7 +16,7 @@ setup(
"prettytable>=3.3.0",
"pycryptodome>=3.4.6",
# "crytic-compile>=0.3.1,<0.4.0",
"crytic-compile@git+https://github.com/crytic/crytic-compile.git@dev#egg=crytic-compile",
"crytic-compile@git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile",
"web3>=6.0.0",
"eth-abi>=4.0.0",
"eth-typing>=3.0.0",
@ -36,7 +36,6 @@ setup(
"coverage[toml]",
"filelock",
"pytest-insta",
"solc-select@git+https://github.com/crytic/solc-select.git@query-artifact-path#egg=solc-select",
],
"doc": [
"pdoc",

@ -442,7 +442,7 @@ def parse_args(
group_checklist.add_argument(
"--checklist-limit",
help="Limite the number of results per detector in the markdown file",
help="Limit the number of results per detector in the markdown file",
action="store",
default="",
)
@ -870,12 +870,6 @@ def main_impl(
logging.error(red(output_error))
logging.error("Please report an issue to https://github.com/crytic/slither/issues")
except Exception: # pylint: disable=broad-except
output_error = traceback.format_exc()
traceback.print_exc()
logging.error(f"Error in {args.filename}") # pylint: disable=logging-fstring-interpolation
logging.error(output_error)
# If we are outputting JSON, capture the redirected output and disable the redirect to output the final JSON.
if outputting_json:
if "console" in args.json_types:

@ -1,4 +1,5 @@
import math
from enum import Enum
from typing import Optional, Dict, List, Set, Union, TYPE_CHECKING, Tuple
from crytic_compile import CompilationUnit, CryticCompile
@ -29,6 +30,20 @@ if TYPE_CHECKING:
from slither.core.slither_core import SlitherCore
class Language(Enum):
SOLIDITY = "solidity"
VYPER = "vyper"
@staticmethod
def from_str(label: str):
if label == "solc":
return Language.SOLIDITY
if label == "vyper":
return Language.VYPER
raise ValueError(f"Unknown language: {label}")
# pylint: disable=too-many-instance-attributes,too-many-public-methods
class SlitherCompilationUnit(Context):
def __init__(self, core: "SlitherCore", crytic_compilation_unit: CompilationUnit) -> None:
@ -36,6 +51,7 @@ class SlitherCompilationUnit(Context):
self._core = core
self._crytic_compile_compilation_unit = crytic_compilation_unit
self._language = Language.from_str(crytic_compilation_unit.compiler_version.compiler)
# Top level object
self.contracts: List[Contract] = []
@ -47,7 +63,7 @@ class SlitherCompilationUnit(Context):
self._pragma_directives: List[Pragma] = []
self._import_directives: List[Import] = []
self._custom_errors: List[CustomErrorTopLevel] = []
self._user_defined_value_types: Dict[str, TypeAliasTopLevel] = {}
self._type_aliases: Dict[str, TypeAliasTopLevel] = {}
self._all_functions: Set[Function] = set()
self._all_modifiers: Set[Modifier] = set()
@ -81,6 +97,17 @@ class SlitherCompilationUnit(Context):
# region Compiler
###################################################################################
###################################################################################
@property
def language(self) -> Language:
return self._language
@property
def is_vyper(self) -> bool:
return self._language == Language.VYPER
@property
def is_solidity(self) -> bool:
return self._language == Language.SOLIDITY
@property
def compiler_version(self) -> CompilerVersion:
@ -166,6 +193,10 @@ class SlitherCompilationUnit(Context):
return self.functions + list(self.modifiers)
def propagate_function_calls(self) -> None:
"""This info is used to compute the rvalues of Phi operations in `fix_phi` and ultimately
is responsible for the `read` property of Phi operations which is vital to
propagating taints inter-procedurally
"""
for f in self.functions_and_modifiers:
for node in f.nodes:
for ir in node.irs_ssa:
@ -220,8 +251,8 @@ class SlitherCompilationUnit(Context):
return self._custom_errors
@property
def user_defined_value_types(self) -> Dict[str, TypeAliasTopLevel]:
return self._user_defined_value_types
def type_aliases(self) -> Dict[str, TypeAliasTopLevel]:
return self._type_aliases
# endregion
###################################################################################
@ -259,6 +290,7 @@ class SlitherCompilationUnit(Context):
###################################################################################
def compute_storage_layout(self) -> None:
assert self.is_solidity
for contract in self.contracts_derived:
self._storage_layouts[contract.name] = {}

@ -45,6 +45,7 @@ if TYPE_CHECKING:
from slither.core.compilation_unit import SlitherCompilationUnit
from slither.core.scope.scope import FileScope
from slither.core.cfg.node import Node
from slither.core.solidity_types import TypeAliasContract
LOGGER = logging.getLogger("Contract")
@ -81,6 +82,7 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
self._functions: Dict[str, "FunctionContract"] = {}
self._linearizedBaseContracts: List[int] = []
self._custom_errors: Dict[str, "CustomErrorContract"] = {}
self._type_aliases: Dict[str, "TypeAliasContract"] = {}
# The only str is "*"
self._using_for: Dict[USING_FOR_KEY, USING_FOR_ITEM] = {}
@ -136,7 +138,7 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
@property
def id(self) -> int:
"""Unique id."""
assert self._id
assert self._id is not None
return self._id
@id.setter
@ -364,6 +366,38 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
def custom_errors_as_dict(self) -> Dict[str, "CustomErrorContract"]:
return self._custom_errors
# endregion
###################################################################################
###################################################################################
# region Custom Errors
###################################################################################
###################################################################################
@property
def type_aliases(self) -> List["TypeAliasContract"]:
"""
list(TypeAliasContract): List of the contract's custom errors
"""
return list(self._type_aliases.values())
@property
def type_aliases_inherited(self) -> List["TypeAliasContract"]:
"""
list(TypeAliasContract): List of the inherited custom errors
"""
return [s for s in self.type_aliases if s.contract != self]
@property
def type_aliases_declared(self) -> List["TypeAliasContract"]:
"""
list(TypeAliasContract): List of the custom errors declared within the contract (not inherited)
"""
return [s for s in self.type_aliases if s.contract == self]
@property
def type_aliases_as_dict(self) -> Dict[str, "TypeAliasContract"]:
return self._type_aliases
# endregion
###################################################################################
###################################################################################

@ -137,6 +137,8 @@ class Function(SourceMapping, metaclass=ABCMeta): # pylint: disable=too-many-pu
self._parameters: List["LocalVariable"] = []
self._parameters_ssa: List["LocalIRVariable"] = []
self._parameters_src: SourceMapping = SourceMapping()
# This is used for vyper calls with default arguments
self._default_args_as_expressions: List["Expression"] = []
self._returns: List["LocalVariable"] = []
self._returns_ssa: List["LocalIRVariable"] = []
self._returns_src: SourceMapping = SourceMapping()
@ -217,8 +219,9 @@ class Function(SourceMapping, metaclass=ABCMeta): # pylint: disable=too-many-pu
self.compilation_unit: "SlitherCompilationUnit" = compilation_unit
# Assume we are analyzing Solidity by default
self.function_language: FunctionLanguage = FunctionLanguage.Solidity
self.function_language: FunctionLanguage = (
FunctionLanguage.Solidity if compilation_unit.is_solidity else FunctionLanguage.Vyper
)
self._id: Optional[str] = None
@ -238,7 +241,7 @@ class Function(SourceMapping, metaclass=ABCMeta): # pylint: disable=too-many-pu
"""
if self._name == "" and self._function_type == FunctionType.CONSTRUCTOR:
return "constructor"
if self._function_type == FunctionType.FALLBACK:
if self._name == "" and self._function_type == FunctionType.FALLBACK:
return "fallback"
if self._function_type == FunctionType.RECEIVE:
return "receive"
@ -985,14 +988,15 @@ class Function(SourceMapping, metaclass=ABCMeta): # pylint: disable=too-many-pu
(str, list(str), list(str)): Function signature as
(name, list parameters type, list return values type)
"""
if self._signature is None:
signature = (
self.name,
[str(x.type) for x in self.parameters],
[str(x.type) for x in self.returns],
)
self._signature = signature
return self._signature
# FIXME memoizing this function is not working properly for vyper
# if self._signature is None:
return (
self.name,
[str(x.type) for x in self.parameters],
[str(x.type) for x in self.returns],
)
# self._signature = signature
# return self._signature
@property
def signature_str(self) -> str:
@ -1497,7 +1501,9 @@ class Function(SourceMapping, metaclass=ABCMeta): # pylint: disable=too-many-pu
Determine if the function can be re-entered
"""
# TODO: compare with hash of known nonReentrant modifier instead of the name
if "nonReentrant" in [m.name for m in self.modifiers]:
if "nonReentrant" in [m.name for m in self.modifiers] or "nonreentrant(lock)" in [
m.name for m in self.modifiers
]:
return False
if self.visibility in ["public", "external"]:
@ -1756,6 +1762,7 @@ class Function(SourceMapping, metaclass=ABCMeta): # pylint: disable=too-many-pu
node.irs_ssa = [ir for ir in node.irs_ssa if not self._unchange_phi(ir)]
def generate_slithir_and_analyze(self) -> None:
for node in self.nodes:
node.slithir_generation()

@ -10,11 +10,14 @@ from slither.exceptions import SlitherException
SOLIDITY_VARIABLES = {
"now": "uint256",
"this": "address",
"self": "address",
"abi": "address", # to simplify the conversion, assume that abi return an address
"msg": "",
"tx": "",
"block": "",
"super": "",
"chain": "",
"ZERO_ADDRESS": "address",
}
SOLIDITY_VARIABLES_COMPOSED = {
@ -34,6 +37,10 @@ SOLIDITY_VARIABLES_COMPOSED = {
"msg.value": "uint256",
"tx.gasprice": "uint256",
"tx.origin": "address",
# Vyper
"chain.id": "uint256",
"block.prevhash": "bytes32",
"self.balance": "uint256",
}
SOLIDITY_FUNCTIONS: Dict[str, List[str]] = {
@ -81,6 +88,32 @@ SOLIDITY_FUNCTIONS: Dict[str, List[str]] = {
"balance(address)": ["uint256"],
"code(address)": ["bytes"],
"codehash(address)": ["bytes32"],
# Vyper
"create_from_blueprint()": [],
"create_minimal_proxy_to()": [],
"empty()": [],
"convert()": [],
"len()": ["uint256"],
"method_id()": [],
"unsafe_sub()": [],
"unsafe_add()": [],
"unsafe_div()": [],
"unsafe_mul()": [],
"pow_mod256()": [],
"max_value()": [],
"min_value()": [],
"concat()": [],
"ecrecover()": [],
"isqrt()": [],
"range()": [],
"min()": [],
"max()": [],
"shift()": [],
"abs()": [],
"raw_call()": ["bool", "bytes32"],
"_abi_encode()": [],
"slice()": [],
"uint2str()": ["string"],
}

@ -12,6 +12,7 @@ from .new_contract import NewContract
from .new_elementary_type import NewElementaryType
from .super_call_expression import SuperCallExpression
from .super_identifier import SuperIdentifier
from .self_identifier import SelfIdentifier
from .tuple_expression import TupleExpression
from .type_conversion import TypeConversion
from .unary_operation import UnaryOperation, UnaryOperationType

@ -42,7 +42,7 @@ class BinaryOperationType(Enum):
# pylint: disable=too-many-branches
@staticmethod
def get_type(
operation_type: "BinaryOperation",
operation_type: "str",
) -> "BinaryOperationType":
if operation_type == "**":
return BinaryOperationType.POWER

@ -4,12 +4,32 @@ from slither.core.expressions.expression import Expression
class CallExpression(Expression): # pylint: disable=too-many-instance-attributes
def __init__(self, called: Expression, arguments: List[Any], type_call: str) -> None:
def __init__(
self,
called: Expression,
arguments: List[Any],
type_call: str,
names: Optional[List[str]] = None,
) -> None:
"""
#### Parameters
called -
The expression denoting the function to be called
arguments -
List of argument expressions
type_call -
A string formatting of the called function's return type
names -
For calls with named fields, list fields in call order.
For calls without named fields, None.
"""
assert isinstance(called, Expression)
assert (names is None) or isinstance(names, list)
super().__init__()
self._called: Expression = called
self._arguments: List[Expression] = arguments
self._type_call: str = type_call
self._names: Optional[List[str]] = names
# gas and value are only available if the syntax is {gas: , value: }
# For the .gas().value(), the member are considered as function call
# And converted later to the correct info (convert.py)
@ -17,6 +37,14 @@ class CallExpression(Expression): # pylint: disable=too-many-instance-attribute
self._value: Optional[Expression] = None
self._salt: Optional[Expression] = None
@property
def names(self) -> Optional[List[str]]:
"""
For calls with named fields, list fields in call order.
For calls without named fields, None.
"""
return self._names
@property
def call_value(self) -> Optional[Expression]:
return self._value
@ -62,4 +90,9 @@ class CallExpression(Expression): # pylint: disable=too-many-instance-attribute
if gas or value or salt:
options = [gas, value, salt]
txt += "{" + ",".join([o for o in options if o != ""]) + "}"
return txt + "(" + ",".join([str(a) for a in self._arguments]) + ")"
args = (
"{" + ",".join([f"{n}:{str(a)}" for (a, n) in zip(self._arguments, self._names)]) + "}"
if self._names is not None
else ",".join([str(a) for a in self._arguments])
)
return txt + "(" + args + ")"

@ -26,7 +26,6 @@ class Identifier(Expression):
],
) -> None:
super().__init__()
# pylint: disable=import-outside-toplevel
from slither.core.declarations import Contract, SolidityVariable, SolidityFunction
from slither.solc_parsing.yul.evm_functions import YulBuiltin

@ -0,0 +1,6 @@
from slither.core.expressions.identifier import Identifier
class SelfIdentifier(Identifier):
def __str__(self):
return "self." + str(self._value)

@ -52,7 +52,7 @@ class FileScope:
# User defined types
# Name -> type alias
self.user_defined_types: Dict[str, TypeAlias] = {}
self.type_aliases: Dict[str, TypeAlias] = {}
def add_accesible_scopes(self) -> bool:
"""
@ -95,8 +95,8 @@ class FileScope:
if not _dict_contain(new_scope.renaming, self.renaming):
self.renaming.update(new_scope.renaming)
learn_something = True
if not _dict_contain(new_scope.user_defined_types, self.user_defined_types):
self.user_defined_types.update(new_scope.user_defined_types)
if not _dict_contain(new_scope.type_aliases, self.type_aliases):
self.type_aliases.update(new_scope.type_aliases)
learn_something = True
return learn_something

@ -1,2 +1,8 @@
from .state_variable import StateVariable
from .variable import Variable
from .local_variable_init_from_tuple import LocalVariableInitFromTuple
from .local_variable import LocalVariable
from .top_level_variable import TopLevelVariable
from .event_variable import EventVariable
from .function_type_variable import FunctionTypeVariable
from .structure_variable import StructureVariable

@ -2,7 +2,6 @@ from typing import Optional, TYPE_CHECKING
from slither.core.variables.variable import Variable
from slither.core.solidity_types.user_defined_type import UserDefinedType
from slither.core.solidity_types.array_type import ArrayType
from slither.core.solidity_types.mapping_type import MappingType
from slither.core.solidity_types.elementary_type import ElementaryType
@ -51,6 +50,9 @@ class LocalVariable(Variable):
Returns:
(bool)
"""
# pylint: disable=import-outside-toplevel
from slither.core.solidity_types.array_type import ArrayType
if self.location == "memory":
return False
if self.location == "calldata":

@ -179,5 +179,6 @@ class Variable(SourceMapping):
return f'{name}({",".join(parameters)})'
def __str__(self) -> str:
assert self._name
if self._name is None:
return ""
return self._name

@ -3,7 +3,7 @@ import re
from logging import Logger
from typing import Optional, List, TYPE_CHECKING, Dict, Union, Callable
from slither.core.compilation_unit import SlitherCompilationUnit
from slither.core.compilation_unit import SlitherCompilationUnit, Language
from slither.core.declarations import Contract
from slither.formatters.exceptions import FormatImpossible
from slither.formatters.utils.patches import apply_patch, create_diff
@ -80,6 +80,9 @@ class AbstractDetector(metaclass=abc.ABCMeta):
# list of vulnerable solc versions as strings (e.g. ["0.4.25", "0.5.0"])
# If the detector is meant to run on all versions, use None
VULNERABLE_SOLC_VERSIONS: Optional[List[str]] = None
# If the detector is meant to run on all languages, use None
# Otherwise, use `solidity` or `vyper`
LANGUAGE: Optional[str] = None
def __init__(
self, compilation_unit: SlitherCompilationUnit, slither: "Slither", logger: Logger
@ -133,6 +136,14 @@ class AbstractDetector(metaclass=abc.ABCMeta):
f"VULNERABLE_SOLC_VERSIONS should not be an empty list {self.__class__.__name__}"
)
if self.LANGUAGE is not None and self.LANGUAGE not in [
Language.SOLIDITY.value,
Language.VYPER.value,
]:
raise IncorrectDetectorInitialization(
f"LANGUAGE should not be either 'solidity' or 'vyper' {self.__class__.__name__}"
)
if re.match("^[a-zA-Z0-9_-]*$", self.ARGUMENT) is None:
raise IncorrectDetectorInitialization(
f"ARGUMENT has illegal character {self.__class__.__name__}"
@ -164,9 +175,14 @@ class AbstractDetector(metaclass=abc.ABCMeta):
if self.logger:
self.logger.info(self.color(info))
def _uses_vulnerable_solc_version(self) -> bool:
def _is_applicable_detector(self) -> bool:
if self.VULNERABLE_SOLC_VERSIONS:
return self.compilation_unit.solc_version in self.VULNERABLE_SOLC_VERSIONS
return (
self.compilation_unit.is_solidity
and self.compilation_unit.solc_version in self.VULNERABLE_SOLC_VERSIONS
)
if self.LANGUAGE:
return self.compilation_unit.language.value == self.LANGUAGE
return True
@abc.abstractmethod
@ -179,7 +195,7 @@ class AbstractDetector(metaclass=abc.ABCMeta):
results: List[Dict] = []
# check solc version
if not self._uses_vulnerable_solc_version():
if not self._is_applicable_detector():
return results
# only keep valid result, and remove duplicate

@ -92,3 +92,8 @@ from .functions.cyclomatic_complexity import CyclomaticComplexity
from .operations.cache_array_length import CacheArrayLength
from .statements.incorrect_using_for import IncorrectUsingFor
from .operations.encode_packed import EncodePackedCollision
from .assembly.incorrect_return import IncorrectReturn
from .assembly.return_instead_of_leave import ReturnInsteadOfLeave
from .operations.incorrect_exp import IncorrectOperatorExponentiation
from .statements.tautological_compare import TautologicalCompare
from .statements.return_bomb import ReturnBomb

@ -0,0 +1,91 @@
from typing import List, Optional
from slither.core.declarations import SolidityFunction, Function
from slither.detectors.abstract_detector import (
AbstractDetector,
DetectorClassification,
DETECTOR_INFO,
)
from slither.slithir.operations import SolidityCall
from slither.utils.output import Output
def _assembly_node(function: Function) -> Optional[SolidityCall]:
"""
Check if there is a node that use return in assembly
Args:
function:
Returns:
"""
for ir in function.all_slithir_operations():
if isinstance(ir, SolidityCall) and ir.function == SolidityFunction(
"return(uint256,uint256)"
):
return ir
return None
class IncorrectReturn(AbstractDetector):
"""
Check for cases where a return(a,b) is used in an assembly function
"""
ARGUMENT = "incorrect-return"
HELP = "If a `return` is incorrectly used in assembly mode."
IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.MEDIUM
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-assembly-return"
WIKI_TITLE = "Incorrect return in assembly"
WIKI_DESCRIPTION = "Detect if `return` in an assembly block halts unexpectedly the execution."
WIKI_EXPLOIT_SCENARIO = """
```solidity
contract C {
function f() internal returns (uint a, uint b) {
assembly {
return (5, 6)
}
}
function g() returns (bool){
f();
return true;
}
}
```
The return statement in `f` will cause execution in `g` to halt.
The function will return 6 bytes starting from offset 5, instead of returning a boolean."""
WIKI_RECOMMENDATION = "Use the `leave` statement."
# pylint: disable=too-many-nested-blocks
def _detect(self) -> List[Output]:
results: List[Output] = []
for c in self.contracts:
for f in c.functions_and_modifiers_declared:
for node in f.nodes:
if node.sons:
for function_called in node.internal_calls:
if isinstance(function_called, Function):
found = _assembly_node(function_called)
if found:
info: DETECTOR_INFO = [
f,
" calls ",
function_called,
" which halt the execution ",
found.node,
"\n",
]
json = self.generate_result(info)
results.append(json)
return results

@ -0,0 +1,68 @@
from typing import List
from slither.core.declarations import SolidityFunction, Function
from slither.detectors.abstract_detector import (
AbstractDetector,
DetectorClassification,
DETECTOR_INFO,
)
from slither.slithir.operations import SolidityCall
from slither.utils.output import Output
class ReturnInsteadOfLeave(AbstractDetector):
"""
Check for cases where a return(a,b) is used in an assembly function that also returns two variables
"""
ARGUMENT = "return-leave"
HELP = "If a `return` is used instead of a `leave`."
IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.MEDIUM
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-assembly-return"
WIKI_TITLE = "Return instead of leave in assembly"
WIKI_DESCRIPTION = "Detect if a `return` is used where a `leave` should be used."
WIKI_EXPLOIT_SCENARIO = """
```solidity
contract C {
function f() internal returns (uint a, uint b) {
assembly {
return (5, 6)
}
}
}
```
The function will halt the execution, instead of returning a two uint."""
WIKI_RECOMMENDATION = "Use the `leave` statement."
def _check_function(self, f: Function) -> List[Output]:
results: List[Output] = []
for node in f.nodes:
for ir in node.irs:
if isinstance(ir, SolidityCall) and ir.function == SolidityFunction(
"return(uint256,uint256)"
):
info: DETECTOR_INFO = [f, " contains an incorrect call to return: ", node, "\n"]
json = self.generate_result(info)
results.append(json)
return results
def _detect(self) -> List[Output]:
results: List[Output] = []
for c in self.contracts:
for f in c.functions_declared:
if (
len(f.returns) == 2
and f.contains_assembly
and f.visibility not in ["public", "external"]
):
results += self._check_function(f)
return results

@ -33,7 +33,7 @@ class IncorrectSolc(AbstractDetector):
HELP = "Incorrect Solidity version"
IMPACT = DetectorClassification.INFORMATIONAL
CONFIDENCE = DetectorClassification.HIGH
LANGUAGE = "solidity"
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-versions-of-solidity"
WIKI_TITLE = "Incorrect versions of Solidity"

@ -24,7 +24,7 @@ class NamingConvention(AbstractDetector):
HELP = "Conformity to Solidity naming conventions"
IMPACT = DetectorClassification.INFORMATIONAL
CONFIDENCE = DetectorClassification.HIGH
LANGUAGE = "solidity"
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#conformance-to-solidity-naming-conventions"
WIKI_TITLE = "Conformance to Solidity naming conventions"
@ -45,6 +45,14 @@ Solidity defines a [naming convention](https://solidity.readthedocs.io/en/v0.4.2
def is_cap_words(name: str) -> bool:
return re.search("^[A-Z]([A-Za-z0-9]+)?_?$", name) is not None
@staticmethod
def is_immutable_naming(name: str) -> bool:
return re.search("^i_[a-z]([A-Za-z0-9]+)?_?$", name) is not None
@staticmethod
def is_state_naming(name: str) -> bool:
return re.search("^s_[a-z]([A-Za-z0-9]+)?_?$", name) is not None
@staticmethod
def is_mixed_case(name: str) -> bool:
return re.search("^[a-z]([A-Za-z0-9]+)?_?$", name) is not None
@ -167,10 +175,17 @@ Solidity defines a [naming convention](https://solidity.readthedocs.io/en/v0.4.2
results.append(res)
else:
if var.visibility == "private":
correct_naming = self.is_mixed_case_with_underscore(var.name)
if var.visibility in ["private", "internal"]:
correct_naming = self.is_mixed_case_with_underscore(
var.name
) or self.is_state_naming(var.name)
if not correct_naming and var.is_immutable:
correct_naming = self.is_immutable_naming(var.name)
else:
correct_naming = self.is_mixed_case(var.name)
if not correct_naming:
info = ["Variable ", var, " is not in mixedCase\n"]

@ -216,9 +216,8 @@ contract C
for usage in non_optimal_array_len_usages:
info = [
"Loop condition ",
f"`{usage.source_mapping.content}` ",
f"({usage.source_mapping}) ",
"should use cached array length instead of referencing `length` member "
usage,
" should use cached array length instead of referencing `length` member "
"of the storage array.\n ",
]
res = self.generate_result(info)

@ -0,0 +1,93 @@
"""
Module detecting incorrect operator usage for exponentiation where bitwise xor '^' is used instead of '**'
"""
from typing import Tuple, List, Union
from slither.core.cfg.node import Node
from slither.core.declarations import Contract, Function
from slither.detectors.abstract_detector import (
AbstractDetector,
DetectorClassification,
DETECTOR_INFO,
)
from slither.slithir.operations import Binary, BinaryType, Operation
from slither.slithir.utils.utils import RVALUE
from slither.slithir.variables.constant import Constant
from slither.utils.output import Output
def _is_constant_candidate(var: Union[RVALUE, Function]) -> bool:
"""
Check if the variable is a constant.
Do not consider variable that are expressed with hexadecimal.
Something like 2^0xf is likely to be a correct bitwise operator
:param var:
:return:
"""
return isinstance(var, Constant) and not var.original_value.startswith("0x")
def _is_bitwise_xor_on_constant(ir: Operation) -> bool:
return (
isinstance(ir, Binary)
and ir.type == BinaryType.CARET
and (_is_constant_candidate(ir.variable_left) or _is_constant_candidate(ir.variable_right))
)
def _detect_incorrect_operator(contract: Contract) -> List[Tuple[Function, Node]]:
ret: List[Tuple[Function, Node]] = []
f: Function
for f in contract.functions + contract.modifiers: # type:ignore
# Heuristic: look for binary expressions with ^ operator where at least one of the operands is a constant, and
# the constant is not in hex, because hex typically is used with bitwise xor and not exponentiation
nodes = [node for node in f.nodes for ir in node.irs if _is_bitwise_xor_on_constant(ir)]
for node in nodes:
ret.append((f, node))
return ret
# pylint: disable=too-few-public-methods
class IncorrectOperatorExponentiation(AbstractDetector):
"""
Incorrect operator usage of bitwise xor mistaking it for exponentiation
"""
ARGUMENT = "incorrect-exp"
HELP = "Incorrect exponentiation"
IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.MEDIUM
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-exponentiation"
WIKI_TITLE = "Incorrect exponentiation"
WIKI_DESCRIPTION = "Detect use of bitwise `xor ^` instead of exponential `**`"
WIKI_EXPLOIT_SCENARIO = """
```solidity
contract Bug{
uint UINT_MAX = 2^256 - 1;
...
}
```
Alice deploys a contract in which `UINT_MAX` incorrectly uses `^` operator instead of `**` for exponentiation"""
WIKI_RECOMMENDATION = "Use the correct operator `**` for exponentiation."
def _detect(self) -> List[Output]:
"""Detect the incorrect operator usage for exponentiation where bitwise xor ^ is used instead of **
Returns:
list: (function, node)
"""
results: List[Output] = []
for c in self.compilation_unit.contracts_derived:
res = _detect_incorrect_operator(c)
for (func, node) in res:
info: DETECTOR_INFO = [
func,
" has bitwise-xor operator ^ instead of the exponentiation operator **: \n",
]
info += ["\t - ", node, "\n"]
results.append(self.generate_result(info))
return results

@ -31,7 +31,7 @@ class DeprecatedStandards(AbstractDetector):
HELP = "Deprecated Solidity Standards"
IMPACT = DetectorClassification.INFORMATIONAL
CONFIDENCE = DetectorClassification.HIGH
LANGUAGE = "solidity"
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#deprecated-standards"
WIKI_TITLE = "Deprecated standards"

@ -2,7 +2,7 @@
Module detecting possible loss of precision due to divide before multiple
"""
from collections import defaultdict
from typing import DefaultDict, List, Set, Tuple
from typing import DefaultDict, List, Tuple
from slither.core.cfg.node import Node
from slither.core.declarations.contract import Contract
@ -63,7 +63,7 @@ def is_assert(node: Node) -> bool:
# pylint: disable=too-many-branches
def _explore(
to_explore: Set[Node], f_results: List[List[Node]], divisions: DefaultDict[LVALUE, List[Node]]
to_explore: List[Node], f_results: List[List[Node]], divisions: DefaultDict[LVALUE, List[Node]]
) -> None:
explored = set()
while to_explore: # pylint: disable=too-many-nested-blocks
@ -114,7 +114,7 @@ def _explore(
f_results.append(node_results)
for son in node.sons:
to_explore.add(son)
to_explore.append(son)
def detect_divide_before_multiply(
@ -145,7 +145,7 @@ def detect_divide_before_multiply(
# track all the division results (and the assignment of the division results)
divisions: DefaultDict[LVALUE, List[Node]] = defaultdict(list)
_explore({function.entry_point}, f_results, divisions)
_explore([function.entry_point], f_results, divisions)
for f_result in f_results:
results.append((function, f_result))

@ -6,6 +6,7 @@ from typing import List, Tuple
from slither.core.cfg.node import Node
from slither.core.declarations import Structure
from slither.core.declarations.contract import Contract
from slither.core.variables.variable import Variable
from slither.core.declarations.function_contract import FunctionContract
from slither.core.solidity_types import MappingType, UserDefinedType
from slither.detectors.abstract_detector import (
@ -69,14 +70,25 @@ The mapping `balances` is never deleted, so `remove` does not work as intended."
for ir in node.irs:
if isinstance(ir, Delete):
value = ir.variable
if isinstance(value.type, UserDefinedType) and isinstance(
value.type.type, Structure
):
st = value.type.type
if any(isinstance(e.type, MappingType) for e in st.elems.values()):
ret.append((f, st, node))
MappingDeletionDetection.check_if_mapping(value, ret, f, node)
return ret
@staticmethod
def check_if_mapping(
value: Variable,
ret: List[Tuple[FunctionContract, Structure, Node]],
f: FunctionContract,
node: Node,
):
if isinstance(value.type, UserDefinedType) and isinstance(value.type.type, Structure):
st = value.type.type
if any(isinstance(e.type, MappingType) for e in st.elems.values()):
ret.append((f, st, node))
return
for e in st.elems.values():
MappingDeletionDetection.check_if_mapping(e, ret, f, node)
def _detect(self) -> List[Output]:
"""Detect mapping deletion

@ -0,0 +1,123 @@
from typing import List
from slither.core.cfg.node import Node
from slither.core.declarations import Contract
from slither.core.declarations.function import Function
from slither.core.solidity_types import Type
from slither.detectors.abstract_detector import (
AbstractDetector,
DetectorClassification,
DETECTOR_INFO,
)
from slither.slithir.operations import LowLevelCall, HighLevelCall
from slither.analyses.data_dependency.data_dependency import is_tainted
from slither.utils.output import Output
class ReturnBomb(AbstractDetector):
ARGUMENT = "return-bomb"
HELP = "A low level callee may consume all callers gas unexpectedly."
IMPACT = DetectorClassification.LOW
CONFIDENCE = DetectorClassification.MEDIUM
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#return-bomb"
WIKI_TITLE = "Return Bomb"
WIKI_DESCRIPTION = "A low level callee may consume all callers gas unexpectedly."
WIKI_EXPLOIT_SCENARIO = """
```solidity
//Modified from https://github.com/nomad-xyz/ExcessivelySafeCall
contract BadGuy {
function youveActivateMyTrapCard() external pure returns (bytes memory) {
assembly{
revert(0, 1000000)
}
}
}
contract Mark {
function oops(address badGuy) public{
bool success;
bytes memory ret;
// Mark pays a lot of gas for this copy
//(success, ret) = badGuy.call{gas:10000}(
(success, ret) = badGuy.call(
abi.encodeWithSelector(
BadGuy.youveActivateMyTrapCard.selector
)
);
// Mark may OOG here, preventing local state changes
//importantCleanup();
}
}
```
After Mark calls BadGuy bytes are copied from returndata to memory, the memory expansion cost is paid. This means that when using a standard solidity call, the callee can "returnbomb" the caller, imposing an arbitrary gas cost.
Callee unexpectedly makes the caller OOG.
"""
WIKI_RECOMMENDATION = "Avoid unlimited implicit decoding of returndata."
@staticmethod
def is_dynamic_type(ty: Type) -> bool:
# ty.is_dynamic ?
name = str(ty)
if "[]" in name or name in ("bytes", "string"):
return True
return False
def get_nodes_for_function(self, function: Function, contract: Contract) -> List[Node]:
nodes = []
for node in function.nodes:
for ir in node.irs:
if isinstance(ir, (HighLevelCall, LowLevelCall)):
if not is_tainted(ir.destination, contract): # type:ignore
# Only interested if the target address is controlled/tainted
continue
if isinstance(ir, HighLevelCall) and isinstance(ir.function, Function):
# in normal highlevel calls return bombs are _possible_
# if the return type is dynamic and the caller tries to copy and decode large data
has_dyn = False
if ir.function.return_type:
has_dyn = any(
self.is_dynamic_type(ty) for ty in ir.function.return_type
)
if not has_dyn:
continue
# If a gas budget was specified then the
# user may not know about the return bomb
if ir.call_gas is None:
# if a gas budget was NOT specified then the caller
# may already suspect the call may spend all gas?
continue
nodes.append(node)
# TODO: check that there is some state change after the call
return nodes
def _detect(self) -> List[Output]:
results = []
for contract in self.compilation_unit.contracts:
for function in contract.functions_declared:
nodes = self.get_nodes_for_function(function, contract)
if nodes:
info: DETECTOR_INFO = [
function,
" tries to limit the gas of an external call that controls implicit decoding\n",
]
for node in sorted(nodes, key=lambda x: x.node_id):
info += ["\t", node, "\n"]
res = self.generate_result(info)
results.append(res)
return results

@ -0,0 +1,69 @@
from typing import List
from slither.detectors.abstract_detector import (
AbstractDetector,
DetectorClassification,
DETECTOR_INFO,
)
from slither.slithir.operations import (
Binary,
BinaryType,
)
from slither.core.declarations import Function
from slither.utils.output import Output
class TautologicalCompare(AbstractDetector):
"""
Same variable comparison detector
"""
ARGUMENT = "tautological-compare"
HELP = "Comparing a variable to itself always returns true or false, depending on comparison"
IMPACT = DetectorClassification.MEDIUM
CONFIDENCE = DetectorClassification.HIGH
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#tautological-compare"
WIKI_TITLE = "Tautological compare"
WIKI_DESCRIPTION = "A variable compared to itself is probably an error as it will always return `true` for `==`, `>=`, `<=` and always `false` for `<`, `>` and `!=`."
WIKI_EXPLOIT_SCENARIO = """
```solidity
function check(uint a) external returns(bool){
return (a >= a);
}
```
`check` always return true."""
WIKI_RECOMMENDATION = "Remove comparison or compare to different value."
def _check_function(self, f: Function) -> List[Output]:
affected_nodes = set()
for node in f.nodes:
for ir in node.irs:
if isinstance(ir, Binary):
if ir.type in [
BinaryType.GREATER,
BinaryType.GREATER_EQUAL,
BinaryType.LESS,
BinaryType.LESS_EQUAL,
BinaryType.EQUAL,
BinaryType.NOT_EQUAL,
]:
if ir.variable_left == ir.variable_right:
affected_nodes.add(node)
results = []
for n in affected_nodes:
info: DETECTOR_INFO = [f, " compares a variable to itself:\n\t", n, "\n"]
res = self.generate_result(info)
results.append(res)
return results
def _detect(self):
results = []
for f in self.compilation_unit.functions_and_modifiers:
results.extend(self._check_function(f))
return results

@ -9,6 +9,8 @@ from .functions.authorization import PrinterWrittenVariablesAndAuthorization
from .summary.slithir import PrinterSlithIR
from .summary.slithir_ssa import PrinterSlithIRSSA
from .summary.human_summary import PrinterHumanSummary
from .summary.ck import CK
from .summary.halstead import Halstead
from .functions.cfg import CFG
from .summary.function_ids import FunctionIds
from .summary.variable_order import VariableOrder
@ -21,3 +23,4 @@ from .summary.evm import PrinterEVM
from .summary.when_not_paused import PrinterWhenNotPaused
from .summary.declaration import Declaration
from .functions.dominator import Dominator
from .summary.martin import Martin

@ -126,15 +126,24 @@ def _extract_constant_functions(slither: SlitherCore) -> Dict[str, List[str]]:
return ret
def _extract_assert(slither: SlitherCore) -> Dict[str, List[str]]:
ret: Dict[str, List[str]] = {}
def _extract_assert(slither: SlitherCore) -> Dict[str, Dict[str, List[Dict]]]:
"""
Return the list of contract -> function name -> List(source mapping of the assert))
Args:
slither:
Returns:
"""
ret: Dict[str, Dict[str, List[Dict]]] = {}
for contract in slither.contracts:
functions_using_assert = []
functions_using_assert: Dict[str, List[Dict]] = defaultdict(list)
for f in contract.functions_entry_points:
for v in f.all_solidity_calls():
if v == SolidityFunction("assert(bool)"):
functions_using_assert.append(_get_name(f))
break
for node in f.all_nodes():
if SolidityFunction("assert(bool)") in node.solidity_calls and node.source_mapping:
func_name = _get_name(f)
functions_using_assert[func_name].append(node.source_mapping.to_json())
if functions_using_assert:
ret[contract.name] = functions_using_assert
return ret

@ -0,0 +1,58 @@
"""
CK Metrics are a suite of six software metrics proposed by Chidamber and Kemerer in 1994.
These metrics are used to measure the complexity of a class.
https://en.wikipedia.org/wiki/Programming_complexity
- Response For a Class (RFC) is a metric that measures the number of unique method calls within a class.
- Number of Children (NOC) is a metric that measures the number of children a class has.
- Depth of Inheritance Tree (DIT) is a metric that measures the number of parent classes a class has.
- Coupling Between Object Classes (CBO) is a metric that measures the number of classes a class is coupled to.
Not implemented:
- Lack of Cohesion of Methods (LCOM) is a metric that measures the lack of cohesion in methods.
- Weighted Methods per Class (WMC) is a metric that measures the complexity of a class.
During the calculation of the metrics above, there are a number of other intermediate metrics that are calculated.
These are also included in the output:
- State variables: total number of state variables
- Constants: total number of constants
- Immutables: total number of immutables
- Public: total number of public functions
- External: total number of external functions
- Internal: total number of internal functions
- Private: total number of private functions
- Mutating: total number of state mutating functions
- View: total number of view functions
- Pure: total number of pure functions
- External mutating: total number of external mutating functions
- No auth or onlyOwner: total number of functions without auth or onlyOwner modifiers
- No modifiers: total number of functions without modifiers
- Ext calls: total number of external calls
"""
from slither.printers.abstract_printer import AbstractPrinter
from slither.utils.ck import CKMetrics
from slither.utils.output import Output
class CK(AbstractPrinter):
ARGUMENT = "ck"
HELP = "Chidamber and Kemerer (CK) complexity metrics and related function attributes"
WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#ck"
def output(self, _filename: str) -> Output:
if len(self.contracts) == 0:
return self.generate_output("No contract found")
ck = CKMetrics(self.contracts)
res = self.generate_output(ck.full_text)
res.add_pretty_table(ck.auxiliary1.pretty_table, ck.auxiliary1.title)
res.add_pretty_table(ck.auxiliary2.pretty_table, ck.auxiliary2.title)
res.add_pretty_table(ck.auxiliary3.pretty_table, ck.auxiliary3.title)
res.add_pretty_table(ck.auxiliary4.pretty_table, ck.auxiliary4.title)
res.add_pretty_table(ck.core.pretty_table, ck.core.title)
self.info(ck.full_text)
return res

@ -0,0 +1,49 @@
"""
Halstead complexity metrics
https://en.wikipedia.org/wiki/Halstead_complexity_measures
12 metrics based on the number of unique operators and operands:
Core metrics:
n1 = the number of distinct operators
n2 = the number of distinct operands
N1 = the total number of operators
N2 = the total number of operands
Extended metrics1:
n = n1 + n2 # Program vocabulary
N = N1 + N2 # Program length
S = n1 * log2(n1) + n2 * log2(n2) # Estimated program length
V = N * log2(n) # Volume
Extended metrics2:
D = (n1 / 2) * (N2 / n2) # Difficulty
E = D * V # Effort
T = E / 18 seconds # Time required to program
B = (E^(2/3)) / 3000 # Number of delivered bugs
"""
from slither.printers.abstract_printer import AbstractPrinter
from slither.utils.halstead import HalsteadMetrics
from slither.utils.output import Output
class Halstead(AbstractPrinter):
ARGUMENT = "halstead"
HELP = "Computes the Halstead complexity metrics for each contract"
WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#halstead"
def output(self, _filename: str) -> Output:
if len(self.contracts) == 0:
return self.generate_output("No contract found")
halstead = HalsteadMetrics(self.contracts)
res = self.generate_output(halstead.full_text)
res.add_pretty_table(halstead.core.pretty_table, halstead.core.title)
res.add_pretty_table(halstead.extended1.pretty_table, halstead.extended1.title)
res.add_pretty_table(halstead.extended2.pretty_table, halstead.extended2.title)
self.info(halstead.full_text)
return res

@ -0,0 +1,32 @@
"""
Robert "Uncle Bob" Martin - Agile software metrics
https://en.wikipedia.org/wiki/Software_package_metrics
Efferent Coupling (Ce): Number of contracts that the contract depends on
Afferent Coupling (Ca): Number of contracts that depend on a contract
Instability (I): Ratio of efferent coupling to total coupling (Ce / (Ce + Ca))
Abstractness (A): Number of abstract contracts / total number of contracts
Distance from the Main Sequence (D): abs(A + I - 1)
"""
from slither.printers.abstract_printer import AbstractPrinter
from slither.utils.martin import MartinMetrics
from slither.utils.output import Output
class Martin(AbstractPrinter):
ARGUMENT = "martin"
HELP = "Martin agile software metrics (Ca, Ce, I, A, D)"
WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#martin"
def output(self, _filename: str) -> Output:
if len(self.contracts) == 0:
return self.generate_output("No contract found")
martin = MartinMetrics(self.contracts)
res = self.generate_output(martin.full_text)
res.add_pretty_table(martin.core.pretty_table, martin.core.title)
self.info(martin.full_text)
return res

@ -11,7 +11,9 @@ from slither.detectors.abstract_detector import AbstractDetector, DetectorClassi
from slither.exceptions import SlitherError
from slither.printers.abstract_printer import AbstractPrinter
from slither.solc_parsing.slither_compilation_unit_solc import SlitherCompilationUnitSolc
from slither.vyper_parsing.vyper_compilation_unit import VyperCompilationUnit
from slither.utils.output import Output
from slither.vyper_parsing.ast.ast import parse
logger = logging.getLogger("Slither")
logging.basicConfig()
@ -62,16 +64,6 @@ class Slither(SlitherCore): # pylint: disable=too-many-instance-attributes
triage_mode (bool): if true, switch to triage mode (default false)
exclude_dependencies (bool): if true, exclude results that are only related to dependencies
generate_patches (bool): if true, patches are generated (json output only)
truffle_ignore (bool): ignore truffle.js presence (default false)
truffle_build_directory (str): build truffle directory (default 'build/contracts')
truffle_ignore_compile (bool): do not run truffle compile (default False)
truffle_version (str): use a specific truffle version (default None)
embark_ignore (bool): ignore embark.js presence (default false)
embark_ignore_compile (bool): do not run embark build (default False)
embark_overwrite_config (bool): overwrite original config file (default false)
change_line_prefix (str): Change the line prefix (default #)
for the displayed source codes (i.e. file.sol#1).
@ -108,13 +100,23 @@ class Slither(SlitherCore): # pylint: disable=too-many-instance-attributes
for compilation_unit in crytic_compile.compilation_units.values():
compilation_unit_slither = SlitherCompilationUnit(self, compilation_unit)
self._compilation_units.append(compilation_unit_slither)
parser = SlitherCompilationUnitSolc(compilation_unit_slither)
self._parsers.append(parser)
for path, ast in compilation_unit.asts.items():
parser.parse_top_level_from_loaded_json(ast, path)
self.add_source_code(path)
_update_file_scopes(compilation_unit_slither.scopes.values())
if compilation_unit_slither.is_vyper:
vyper_parser = VyperCompilationUnit(compilation_unit_slither)
for path, ast in compilation_unit.asts.items():
ast_nodes = parse(ast["ast"])
vyper_parser.parse_module(ast_nodes, path)
self._parsers.append(vyper_parser)
else:
# Solidity specific
assert compilation_unit_slither.is_solidity
sol_parser = SlitherCompilationUnitSolc(compilation_unit_slither)
self._parsers.append(sol_parser)
for path, ast in compilation_unit.asts.items():
sol_parser.parse_top_level_items(ast, path)
self.add_source_code(path)
_update_file_scopes(compilation_unit_slither.scopes.values())
if kwargs.get("generate_patches", False):
self.generate_patches = True

@ -114,8 +114,8 @@ def convert_expression(expression: Expression, node: "Node") -> List[Operation]:
visitor = ExpressionToSlithIR(expression, node)
result = visitor.result()
result = apply_ir_heuristics(result, node)
is_solidity = node.compilation_unit.is_solidity
result = apply_ir_heuristics(result, node, is_solidity)
if result:
if node.type in [NodeType.IF, NodeType.IFLOOP]:
@ -385,6 +385,70 @@ def integrate_value_gas(result: List[Operation]) -> List[Operation]:
###################################################################################
def get_declared_param_names(
ins: Union[
NewStructure,
NewContract,
InternalCall,
LibraryCall,
HighLevelCall,
InternalDynamicCall,
EventCall,
]
) -> Optional[List[str]]:
"""
Given a call operation, return the list of parameter names, in the order
listed in the function declaration.
#### Parameters
ins -
The call instruction
#### Possible Returns
List[str] -
A list of the parameters in declaration order
None -
Workaround: Unable to obtain list of parameters in declaration order
"""
if isinstance(ins, NewStructure):
return [x.name for x in ins.structure.elems_ordered if not isinstance(x.type, MappingType)]
if isinstance(ins, (InternalCall, LibraryCall, HighLevelCall)):
if isinstance(ins.function, Function):
return [p.name for p in ins.function.parameters]
return None
if isinstance(ins, InternalDynamicCall):
return [p.name for p in ins.function_type.params]
assert isinstance(ins, (EventCall, NewContract))
return None
def reorder_arguments(
args: List[Variable], call_names: List[str], decl_names: List[str]
) -> List[Variable]:
"""
Reorder named struct constructor arguments so that they match struct declaration ordering rather
than call ordering
E.g. for `struct S { int x; int y; }` we reorder `S({y : 2, x : 3})` to `S(3, 2)`
#### Parameters
args -
Arguments to constructor call, in call order
names -
Parameter names in call order
decl_names -
Parameter names in declaration order
#### Returns
Reordered arguments to constructor call, now in declaration order
"""
assert len(args) == len(call_names)
assert len(call_names) == len(decl_names)
args_ret = []
for n in decl_names:
ind = call_names.index(n)
args_ret.append(args[ind])
return args_ret
def propagate_type_and_convert_call(result: List[Operation], node: "Node") -> List[Operation]:
"""
Propagate the types variables and convert tmp call to real call operation
@ -434,6 +498,23 @@ def propagate_type_and_convert_call(result: List[Operation], node: "Node") -> Li
if ins.call_id in calls_gas and isinstance(ins, (HighLevelCall, InternalDynamicCall)):
ins.call_gas = calls_gas[ins.call_id]
if isinstance(ins, Call) and (ins.names is not None):
assert isinstance(
ins,
(
NewStructure,
NewContract,
InternalCall,
LibraryCall,
HighLevelCall,
InternalDynamicCall,
EventCall,
),
)
decl_param_names = get_declared_param_names(ins)
if decl_param_names is not None:
call_data = reorder_arguments(call_data, ins.names, decl_param_names)
if isinstance(ins, (Call, NewContract, NewStructure)):
# We might have stored some arguments for libraries
if ins.arguments:
@ -576,7 +657,9 @@ def propagate_types(ir: Operation, node: "Node"): # pylint: disable=too-many-lo
if isinstance(t, ArrayType) or (
isinstance(t, ElementaryType) and t.type == "bytes"
):
if ir.function_name == "push" and len(ir.arguments) <= 1:
# Solidity uses push
# Vyper uses append
if ir.function_name in ["push", "append"] and len(ir.arguments) <= 1:
return convert_to_push(ir, node)
if ir.function_name == "pop" and len(ir.arguments) == 0:
return convert_to_pop(ir, node)
@ -855,7 +938,7 @@ def extract_tmp_call(ins: TmpCall, contract: Optional[Contract]) -> Union[Call,
if isinstance(ins.ori.variable_left, Contract):
st = ins.ori.variable_left.get_structure_from_name(ins.ori.variable_right)
if st:
op = NewStructure(st, ins.lvalue)
op = NewStructure(st, ins.lvalue, names=ins.names)
op.set_expression(ins.expression)
op.call_id = ins.call_id
return op
@ -892,6 +975,7 @@ def extract_tmp_call(ins: TmpCall, contract: Optional[Contract]) -> Union[Call,
ins.nbr_arguments,
ins.lvalue,
ins.type_call,
names=ins.names,
)
libcall.set_expression(ins.expression)
libcall.call_id = ins.call_id
@ -950,6 +1034,7 @@ def extract_tmp_call(ins: TmpCall, contract: Optional[Contract]) -> Union[Call,
len(lib_func.parameters),
ins.lvalue,
"d",
names=ins.names,
)
lib_call.set_expression(ins.expression)
lib_call.set_node(ins.node)
@ -1031,6 +1116,7 @@ def extract_tmp_call(ins: TmpCall, contract: Optional[Contract]) -> Union[Call,
ins.nbr_arguments,
ins.lvalue,
ins.type_call,
names=ins.names,
)
msgcall.call_id = ins.call_id
@ -1082,7 +1168,7 @@ def extract_tmp_call(ins: TmpCall, contract: Optional[Contract]) -> Union[Call,
return n
if isinstance(ins.called, Structure):
op = NewStructure(ins.called, ins.lvalue)
op = NewStructure(ins.called, ins.lvalue, names=ins.names)
op.set_expression(ins.expression)
op.call_id = ins.call_id
op.set_expression(ins.expression)
@ -1106,7 +1192,7 @@ def extract_tmp_call(ins: TmpCall, contract: Optional[Contract]) -> Union[Call,
if len(ins.called.constructor.parameters) != ins.nbr_arguments:
return Nop()
internalcall = InternalCall(
ins.called.constructor, ins.nbr_arguments, ins.lvalue, ins.type_call
ins.called.constructor, ins.nbr_arguments, ins.lvalue, ins.type_call, ins.names
)
internalcall.call_id = ins.call_id
internalcall.set_expression(ins.expression)
@ -1131,6 +1217,7 @@ def can_be_low_level(ir: HighLevelCall) -> bool:
"delegatecall",
"callcode",
"staticcall",
"raw_call",
]
@ -1159,13 +1246,14 @@ def convert_to_low_level(
ir.set_node(prev_ir.node)
ir.lvalue.set_type(ElementaryType("bool"))
return ir
if ir.function_name in ["call", "delegatecall", "callcode", "staticcall"]:
if ir.function_name in ["call", "delegatecall", "callcode", "staticcall", "raw_call"]:
new_ir = LowLevelCall(
ir.destination, ir.function_name, ir.nbr_arguments, ir.lvalue, ir.type_call
)
new_ir.call_gas = ir.call_gas
new_ir.call_value = ir.call_value
new_ir.arguments = ir.arguments
# TODO fix this for Vyper
if ir.node.compilation_unit.solc_version >= "0.5":
new_ir.lvalue.set_type([ElementaryType("bool"), ElementaryType("bytes")])
else:
@ -1210,7 +1298,12 @@ def convert_to_solidity_func(
and len(new_ir.arguments) == 2
and isinstance(new_ir.arguments[1], list)
):
types = list(new_ir.arguments[1])
types = []
for arg_type in new_ir.arguments[1]:
decode_type = arg_type
if isinstance(decode_type, (Structure, Enum, Contract)):
decode_type = UserDefinedType(decode_type)
types.append(decode_type)
new_ir.lvalue.set_type(types)
# abi.decode where the type to decode is a singleton
# abi.decode(a, (uint))
@ -1440,6 +1533,7 @@ def look_for_library_or_top_level(
ir.nbr_arguments,
ir.lvalue,
ir.type_call,
names=ir.names,
)
lib_call.set_expression(ir.expression)
lib_call.set_node(ir.node)
@ -1857,7 +1951,7 @@ def convert_constant_types(irs: List[Operation]) -> None:
if isinstance(ir.lvalue.type.type, ElementaryType):
if ir.lvalue.type.type.type in ElementaryTypeInt:
for r in ir.read:
if r.type.type not in ElementaryTypeInt:
if r.type.type.type not in ElementaryTypeInt:
r.set_type(ElementaryType(ir.lvalue.type.type.type))
was_changed = True
@ -1906,7 +2000,7 @@ def _find_source_mapping_references(irs: List[Operation]) -> None:
###################################################################################
def apply_ir_heuristics(irs: List[Operation], node: "Node") -> List[Operation]:
def apply_ir_heuristics(irs: List[Operation], node: "Node", is_solidity: bool) -> List[Operation]:
"""
Apply a set of heuristic to improve slithIR
"""
@ -1916,8 +2010,11 @@ def apply_ir_heuristics(irs: List[Operation], node: "Node") -> List[Operation]:
irs = propagate_type_and_convert_call(irs, node)
irs = remove_unused(irs)
find_references_origin(irs)
convert_constant_types(irs)
convert_delete(irs)
# These are heuristics that are only applied to Solidity
if is_solidity:
convert_constant_types(irs)
convert_delete(irs)
_find_source_mapping_references(irs)

@ -6,9 +6,25 @@ from slither.slithir.operations.operation import Operation
class Call(Operation):
def __init__(self) -> None:
def __init__(self, names: Optional[List[str]] = None) -> None:
"""
#### Parameters
names -
For calls of the form f({argName1 : arg1, ...}), the names of parameters listed in call order.
Otherwise, None.
"""
assert (names is None) or isinstance(names, list)
super().__init__()
self._arguments: List[Variable] = []
self._names = names
@property
def names(self) -> Optional[List[str]]:
"""
For calls of the form f({argName1 : arg1, ...}), the names of parameters listed in call order.
Otherwise, None.
"""
return self._names
@property
def arguments(self) -> List[Variable]:

@ -28,11 +28,18 @@ class HighLevelCall(Call, OperationWithLValue):
nbr_arguments: int,
result: Optional[Union[TemporaryVariable, TupleVariable, TemporaryVariableSSA]],
type_call: str,
names: Optional[List[str]] = None,
) -> None:
"""
#### Parameters
names -
For calls of the form f({argName1 : arg1, ...}), the names of parameters listed in call order.
Otherwise, None.
"""
assert isinstance(function_name, Constant)
assert is_valid_lvalue(result) or result is None
self._check_destination(destination)
super().__init__()
super().__init__(names=names)
# Contract is only possible for library call, which inherits from highlevelcall
self._destination: Union[Variable, SolidityVariable, Contract] = destination # type: ignore
self._function_name = function_name

@ -44,4 +44,4 @@ class InitArray(OperationWithLValue):
return f"{elem}({elem.type})"
init_values = convert(self.init_values)
return f"{self.lvalue}({self.lvalue.type}) = {init_values}"
return f"{self.lvalue}({self.lvalue.type}) = {init_values}"

@ -20,8 +20,16 @@ class InternalCall(Call, OperationWithLValue): # pylint: disable=too-many-insta
Union[TupleVariableSSA, TemporaryVariableSSA, TupleVariable, TemporaryVariable]
],
type_call: str,
names: Optional[List[str]] = None,
) -> None:
super().__init__()
# pylint: disable=too-many-arguments
"""
#### Parameters
names -
For calls of the form f({argName1 : arg1, ...}), the names of parameters listed in call order.
Otherwise, None.
"""
super().__init__(names=names)
self._contract_name = ""
if isinstance(function, Function):
self._function: Optional[Function] = function

@ -33,4 +33,4 @@ class NewArray(Call, OperationWithLValue):
def __str__(self):
args = [str(a) for a in self.arguments]
lvalue = self.lvalue
return f"{lvalue}{lvalue.type}) = new {self.array_type}({','.join(args)})"
return f"{lvalue}({lvalue.type}) = new {self.array_type}({','.join(args)})"

@ -12,11 +12,20 @@ from slither.slithir.variables.temporary_ssa import TemporaryVariableSSA
class NewContract(Call, OperationWithLValue): # pylint: disable=too-many-instance-attributes
def __init__(
self, contract_name: Constant, lvalue: Union[TemporaryVariableSSA, TemporaryVariable]
self,
contract_name: Constant,
lvalue: Union[TemporaryVariableSSA, TemporaryVariable],
names: Optional[List[str]] = None,
) -> None:
"""
#### Parameters
names -
For calls of the form f({argName1 : arg1, ...}), the names of parameters listed in call order.
Otherwise, None.
"""
assert isinstance(contract_name, Constant)
assert is_valid_lvalue(lvalue)
super().__init__()
super().__init__(names=names)
self._contract_name = contract_name
# todo create analyze to add the contract instance
self._lvalue = lvalue

@ -1,4 +1,4 @@
from typing import List, Union
from typing import List, Optional, Union
from slither.slithir.operations.call import Call
from slither.slithir.operations.lvalue import OperationWithLValue
@ -17,8 +17,15 @@ class NewStructure(Call, OperationWithLValue):
self,
structure: StructureContract,
lvalue: Union[TemporaryVariableSSA, TemporaryVariable],
names: Optional[List[str]] = None,
) -> None:
super().__init__()
"""
#### Parameters
names -
For calls of the form f({argName1 : arg1, ...}), the names of parameters listed in call order.
Otherwise, None.
"""
super().__init__(names=names)
assert isinstance(structure, Structure)
assert is_valid_lvalue(lvalue)
self._structure = structure

@ -4,6 +4,7 @@ from slither.core.declarations import Contract
from slither.core.solidity_types.elementary_type import ElementaryType
from slither.core.solidity_types.type_alias import TypeAlias
from slither.core.solidity_types.user_defined_type import UserDefinedType
from slither.core.solidity_types.array_type import ArrayType
from slither.core.source_mapping.source_mapping import SourceMapping
from slither.slithir.operations.lvalue import OperationWithLValue
from slither.slithir.utils.utils import is_valid_lvalue, is_valid_rvalue
@ -21,10 +22,10 @@ class TypeConversion(OperationWithLValue):
super().__init__()
assert is_valid_rvalue(variable) or isinstance(variable, Contract)
assert is_valid_lvalue(result)
assert isinstance(variable_type, (TypeAlias, UserDefinedType, ElementaryType))
assert isinstance(variable_type, (TypeAlias, UserDefinedType, ElementaryType, ArrayType))
self._variable = variable
self._type: Union[TypeAlias, UserDefinedType, ElementaryType] = variable_type
self._type: Union[TypeAlias, UserDefinedType, ElementaryType, ArrayType] = variable_type
self._lvalue = result
@property

@ -5,7 +5,6 @@ from enum import Enum
from slither.slithir.operations.lvalue import OperationWithLValue
from slither.slithir.utils.utils import is_valid_lvalue, is_valid_rvalue
from slither.slithir.exceptions import SlithIRError
from slither.core.expressions.unary_operation import UnaryOperationType
from slither.core.variables.local_variable import LocalVariable
from slither.slithir.variables.constant import Constant
from slither.slithir.variables.local_variable import LocalIRVariable
@ -35,7 +34,7 @@ class Unary(OperationWithLValue):
self,
result: Union[TemporaryVariableSSA, TemporaryVariable],
variable: Union[Constant, LocalIRVariable, LocalVariable],
operation_type: UnaryOperationType,
operation_type: UnaryType,
) -> None:
assert is_valid_rvalue(variable)
assert is_valid_lvalue(result)
@ -53,7 +52,7 @@ class Unary(OperationWithLValue):
return self._variable
@property
def type(self) -> UnaryOperationType:
def type(self) -> UnaryType:
return self._type
@property

@ -1,4 +1,4 @@
from typing import Optional, Union
from typing import List, Optional, Union
from slither.core.declarations import (
Event,
@ -25,7 +25,15 @@ class TmpCall(OperationWithLValue): # pylint: disable=too-many-instance-attribu
nbr_arguments: int,
result: Union[TupleVariable, TemporaryVariable],
type_call: str,
names: Optional[List[str]] = None,
) -> None:
# pylint: disable=too-many-arguments
"""
#### Parameters
names -
For calls of the form f({argName1 : arg1, ...}), the names of parameters listed in call order.
Otherwise, None.
"""
assert isinstance(
called,
(
@ -42,6 +50,7 @@ class TmpCall(OperationWithLValue): # pylint: disable=too-many-instance-attribu
self._called = called
self._nbr_arguments = nbr_arguments
self._type_call = type_call
self._names = names
self._lvalue = result
self._ori = None #
self._callid = None
@ -49,6 +58,14 @@ class TmpCall(OperationWithLValue): # pylint: disable=too-many-instance-attribu
self._value = None
self._salt = None
@property
def names(self) -> Optional[List[str]]:
"""
For calls of the form f({argName1 : arg1, ...}), the names of parameters listed in call order.
Otherwise, None.
"""
return self._names
@property
def call_value(self):
return self._value

@ -735,12 +735,17 @@ def copy_ir(ir: Operation, *instances) -> Operation:
destination = get_variable(ir, lambda x: x.destination, *instances)
function_name = ir.function_name
nbr_arguments = ir.nbr_arguments
names = ir.names
lvalue = get_variable(ir, lambda x: x.lvalue, *instances)
type_call = ir.type_call
if isinstance(ir, LibraryCall):
new_ir = LibraryCall(destination, function_name, nbr_arguments, lvalue, type_call)
new_ir = LibraryCall(
destination, function_name, nbr_arguments, lvalue, type_call, names=names
)
else:
new_ir = HighLevelCall(destination, function_name, nbr_arguments, lvalue, type_call)
new_ir = HighLevelCall(
destination, function_name, nbr_arguments, lvalue, type_call, names=names
)
new_ir.call_id = ir.call_id
new_ir.call_value = get_variable(ir, lambda x: x.call_value, *instances)
new_ir.call_gas = get_variable(ir, lambda x: x.call_gas, *instances)
@ -761,7 +766,8 @@ def copy_ir(ir: Operation, *instances) -> Operation:
nbr_arguments = ir.nbr_arguments
lvalue = get_variable(ir, lambda x: x.lvalue, *instances)
type_call = ir.type_call
new_ir = InternalCall(function, nbr_arguments, lvalue, type_call)
names = ir.names
new_ir = InternalCall(function, nbr_arguments, lvalue, type_call, names=names)
new_ir.arguments = get_arguments(ir, *instances)
return new_ir
if isinstance(ir, InternalDynamicCall):
@ -811,7 +817,8 @@ def copy_ir(ir: Operation, *instances) -> Operation:
if isinstance(ir, NewStructure):
structure = ir.structure
lvalue = get_variable(ir, lambda x: x.lvalue, *instances)
new_ir = NewStructure(structure, lvalue)
names = ir.names
new_ir = NewStructure(structure, lvalue, names=names)
new_ir.arguments = get_arguments(ir, *instances)
return new_ir
if isinstance(ir, Nop):

@ -11,7 +11,7 @@ from slither.utils.integer_conversion import convert_string_to_int
class Constant(SlithIRVariable):
def __init__(
self,
val: Union[int, str],
val: str,
constant_type: Optional[ElementaryType] = None,
subdenomination: Optional[str] = None,
) -> None: # pylint: disable=too-many-branches

@ -291,10 +291,10 @@ class ContractSolc(CallerContextExpression):
alias = item["name"]
alias_canonical = self._contract.name + "." + item["name"]
user_defined_type = TypeAliasContract(original_type, alias, self.underlying_contract)
user_defined_type.set_offset(item["src"], self.compilation_unit)
self._contract.file_scope.user_defined_types[alias] = user_defined_type
self._contract.file_scope.user_defined_types[alias_canonical] = user_defined_type
type_alias = TypeAliasContract(original_type, alias, self.underlying_contract)
type_alias.set_offset(item["src"], self.compilation_unit)
self._contract.type_aliases_as_dict[alias] = type_alias
self._contract.file_scope.type_aliases[alias_canonical] = type_alias
def _parse_struct(self, struct: Dict) -> None:
@ -319,7 +319,7 @@ class ContractSolc(CallerContextExpression):
ce.set_contract(self._contract)
ce.set_offset(custom_error["src"], self.compilation_unit)
ce_parser = CustomErrorSolc(ce, custom_error, self._slither_parser)
ce_parser = CustomErrorSolc(ce, custom_error, self, self._slither_parser)
self._contract.custom_errors_as_dict[ce.name] = ce
self._custom_errors_parser.append(ce_parser)

@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Dict
from typing import TYPE_CHECKING, Dict, Optional
from slither.core.declarations.custom_error import CustomError
from slither.core.declarations.custom_error_contract import CustomErrorContract
@ -10,6 +10,7 @@ from slither.solc_parsing.variables.local_variable import LocalVariableSolc
if TYPE_CHECKING:
from slither.solc_parsing.slither_compilation_unit_solc import SlitherCompilationUnitSolc
from slither.core.compilation_unit import SlitherCompilationUnit
from slither.solc_parsing.declarations.contract import ContractSolc
# Part of the code was copied from the function parsing
@ -21,11 +22,13 @@ class CustomErrorSolc(CallerContextExpression):
self,
custom_error: CustomError,
custom_error_data: dict,
contract_parser: Optional["ContractSolc"],
slither_parser: "SlitherCompilationUnitSolc",
) -> None:
self._slither_parser: "SlitherCompilationUnitSolc" = slither_parser
self._custom_error = custom_error
custom_error.name = custom_error_data["name"]
self._contract_parser = contract_parser
self._params_was_analyzed = False
if not self._slither_parser.is_compact_ast:
@ -56,6 +59,10 @@ class CustomErrorSolc(CallerContextExpression):
if params:
self._parse_params(params)
@property
def contract_parser(self) -> Optional["ContractSolc"]:
return self._contract_parser
@property
def is_compact_ast(self) -> bool:
return self._slither_parser.is_compact_ast

@ -152,7 +152,7 @@ class UsingForTopLevelSolc(CallerContextExpression): # pylint: disable=too-few-
if self._global:
for scope in self.compilation_unit.scopes.values():
if isinstance(type_name, TypeAliasTopLevel):
for alias in scope.user_defined_types.values():
for alias in scope.type_aliases.values():
if alias == type_name:
scope.using_for_directives.add(self._using_for)
elif isinstance(type_name, UserDefinedType):

@ -175,11 +175,12 @@ def parse_call(
called = parse_expression(children[0], caller_context)
arguments = [parse_expression(a, caller_context) for a in children[1::]]
if isinstance(called, SuperCallExpression):
if isinstance(called, SuperIdentifier):
sp = SuperCallExpression(called, arguments, type_return)
sp.set_offset(expression["src"], caller_context.compilation_unit)
return sp
call_expression = CallExpression(called, arguments, type_return)
names = expression["names"] if "names" in expression and len(expression["names"]) > 0 else None
call_expression = CallExpression(called, arguments, type_return, names=names)
call_expression.set_offset(src, caller_context.compilation_unit)
# Only available if the syntax {gas:, value:} was used

@ -3,6 +3,8 @@ from typing import TYPE_CHECKING, Optional, Union, List, Tuple
from slither.core.declarations import Event, Enum, Structure
from slither.core.declarations.contract import Contract
from slither.core.declarations.custom_error import CustomError
from slither.core.declarations.custom_error_contract import CustomErrorContract
from slither.core.declarations.custom_error_top_level import CustomErrorTopLevel
from slither.core.declarations.function import Function
from slither.core.declarations.function_contract import FunctionContract
from slither.core.declarations.function_top_level import FunctionTopLevel
@ -114,6 +116,8 @@ def find_top_level(
:return:
:rtype:
"""
if var_name in scope.type_aliases:
return scope.type_aliases[var_name], False
if var_name in scope.structures:
return scope.structures[var_name], False
@ -205,6 +209,10 @@ def _find_in_contract(
if sig == var_name:
return modifier
type_aliases = contract.type_aliases_as_dict
if var_name in type_aliases:
return type_aliases[var_name]
# structures are looked on the contract declarer
structures = contract.structures_as_dict
if var_name in structures:
@ -240,6 +248,7 @@ def _find_in_contract(
return None
# pylint: disable=too-many-statements
def _find_variable_init(
caller_context: CallerContextExpression,
) -> Tuple[List[Contract], List["Function"], FileScope,]:
@ -247,6 +256,7 @@ def _find_variable_init(
from slither.solc_parsing.declarations.function import FunctionSolc
from slither.solc_parsing.declarations.structure_top_level import StructureTopLevelSolc
from slither.solc_parsing.variables.top_level_variable import TopLevelVariableSolc
from slither.solc_parsing.declarations.custom_error import CustomErrorSolc
direct_contracts: List[Contract]
direct_functions_parser: List[Function]
@ -289,6 +299,24 @@ def _find_variable_init(
direct_contracts = []
direct_functions_parser = []
scope = caller_context.underlying_variable.file_scope
elif isinstance(caller_context, CustomErrorSolc):
if caller_context.contract_parser:
direct_contracts = [caller_context.contract_parser.underlying_contract]
direct_functions_parser = [
f.underlying_function
for f in caller_context.contract_parser.functions_parser
+ caller_context.contract_parser.modifiers_parser
]
else:
# Top level custom error
direct_contracts = []
direct_functions_parser = []
underlying_custom_error = caller_context.underlying_custom_error
if isinstance(underlying_custom_error, CustomErrorTopLevel):
scope = underlying_custom_error.file_scope
else:
assert isinstance(underlying_custom_error, CustomErrorContract)
scope = underlying_custom_error.contract.file_scope
else:
raise SlitherError(
f"{type(caller_context)} ({caller_context} is not valid for find_variable"
@ -337,6 +365,7 @@ def find_variable(
"""
from slither.solc_parsing.declarations.function import FunctionSolc
from slither.solc_parsing.declarations.contract import ContractSolc
from slither.solc_parsing.declarations.custom_error import CustomErrorSolc
# variable are looked from the contract declarer
# functions can be shadowed, but are looked from the contract instance, rather than the contract declarer
@ -362,9 +391,6 @@ def find_variable(
if var_name in current_scope.renaming:
var_name = current_scope.renaming[var_name]
if var_name in current_scope.user_defined_types:
return current_scope.user_defined_types[var_name], False
# Use ret0/ret1 to help mypy
ret0 = _find_variable_from_ref_declaration(
referenced_declaration, direct_contracts, direct_functions
@ -391,6 +417,15 @@ def find_variable(
contract_declarer = underlying_func.contract_declarer
else:
assert isinstance(underlying_func, FunctionTopLevel)
elif isinstance(caller_context, CustomErrorSolc):
underlying_custom_error = caller_context.underlying_custom_error
if isinstance(underlying_custom_error, CustomErrorContract):
contract = underlying_custom_error.contract
# We check for contract variables here because _find_in_contract
# will return since in this case the contract_declarer is None
for var in contract.variables:
if var_name == var.name:
return var, False
ret = _find_in_contract(var_name, contract, contract_declarer, is_super, is_identifier_path)
if ret:

@ -75,9 +75,12 @@ class SlitherCompilationUnitSolc(CallerContextExpression):
def __init__(self, compilation_unit: SlitherCompilationUnit) -> None:
super().__init__()
self._compilation_unit: SlitherCompilationUnit = compilation_unit
self._contracts_by_id: Dict[int, ContractSolc] = {}
self._parsed = False
self._analyzed = False
self._is_compact_ast = False
self._underlying_contract_to_parser: Dict[Contract, ContractSolc] = {}
self._structures_top_level_parser: List[StructureTopLevelSolc] = []
@ -85,11 +88,6 @@ class SlitherCompilationUnitSolc(CallerContextExpression):
self._variables_top_level_parser: List[TopLevelVariableSolc] = []
self._functions_top_level_parser: List[FunctionSolc] = []
self._using_for_top_level_parser: List[UsingForTopLevelSolc] = []
self._is_compact_ast = False
# self._core: SlitherCore = core
self._compilation_unit = compilation_unit
self._all_functions_and_modifier_parser: List[FunctionSolc] = []
self._top_level_contracts_counter = 0
@ -145,14 +143,14 @@ class SlitherCompilationUnitSolc(CallerContextExpression):
data_loaded = json.loads(json_data)
# Truffle AST
if "ast" in data_loaded:
self.parse_top_level_from_loaded_json(data_loaded["ast"], data_loaded["sourcePath"])
self.parse_top_level_items(data_loaded["ast"], data_loaded["sourcePath"])
return True
# solc AST, where the non-json text was removed
if "attributes" in data_loaded:
filename = data_loaded["attributes"]["absolutePath"]
else:
filename = data_loaded["absolutePath"]
self.parse_top_level_from_loaded_json(data_loaded, filename)
self.parse_top_level_items(data_loaded, filename)
return True
except ValueError:
@ -163,7 +161,7 @@ class SlitherCompilationUnitSolc(CallerContextExpression):
json_data = json_data[first:last]
data_loaded = json.loads(json_data)
self.parse_top_level_from_loaded_json(data_loaded, filename)
self.parse_top_level_items(data_loaded, filename)
return True
return False
@ -197,7 +195,7 @@ class SlitherCompilationUnitSolc(CallerContextExpression):
self._compilation_unit.enums_top_level.append(enum)
# pylint: disable=too-many-branches,too-many-statements,too-many-locals
def parse_top_level_from_loaded_json(self, data_loaded: Dict, filename: str) -> None:
def parse_top_level_items(self, data_loaded: Dict, filename: str) -> None:
if not data_loaded or data_loaded is None:
logger.error(
"crytic-compile returned an empty AST. "
@ -326,7 +324,7 @@ class SlitherCompilationUnitSolc(CallerContextExpression):
custom_error = CustomErrorTopLevel(self._compilation_unit, scope)
custom_error.set_offset(top_level_data["src"], self._compilation_unit)
custom_error_parser = CustomErrorSolc(custom_error, top_level_data, self)
custom_error_parser = CustomErrorSolc(custom_error, top_level_data, None, self)
scope.custom_errors.add(custom_error)
self._compilation_unit.custom_errors.append(custom_error)
self._custom_error_parser.append(custom_error_parser)
@ -344,10 +342,10 @@ class SlitherCompilationUnitSolc(CallerContextExpression):
original_type = ElementaryType(underlying_type["name"])
user_defined_type = TypeAliasTopLevel(original_type, alias, scope)
user_defined_type.set_offset(top_level_data["src"], self._compilation_unit)
self._compilation_unit.user_defined_value_types[alias] = user_defined_type
scope.user_defined_types[alias] = user_defined_type
type_alias = TypeAliasTopLevel(original_type, alias, scope)
type_alias.set_offset(top_level_data["src"], self._compilation_unit)
self._compilation_unit.type_aliases[alias] = type_alias
scope.type_aliases[alias] = type_alias
else:
raise SlitherException(f"Top level {top_level_data[self.get_key()]} not supported")
@ -405,7 +403,7 @@ class SlitherCompilationUnitSolc(CallerContextExpression):
def parse_contracts(self) -> None: # pylint: disable=too-many-statements,too-many-branches
if not self._underlying_contract_to_parser:
logger.info(
f"No contract were found in {self._compilation_unit.core.filename}, check the correct compilation"
f"No contracts were found in {self._compilation_unit.core.filename}, check the correct compilation"
)
if self._parsed:
raise Exception("Contract analysis can be run only once!")

@ -235,7 +235,7 @@ def parse_type(
sl: "SlitherCompilationUnit"
renaming: Dict[str, str]
user_defined_types: Dict[str, TypeAlias]
type_aliases: Dict[str, TypeAlias]
enums_direct_access: List["Enum"] = []
# Note: for convenicence top level functions use the same parser than function in contract
# but contract_parser is set to None
@ -247,13 +247,13 @@ def parse_type(
sl = caller_context.compilation_unit
next_context = caller_context
renaming = {}
user_defined_types = sl.user_defined_value_types
type_aliases = sl.type_aliases
else:
assert isinstance(caller_context, FunctionSolc)
sl = caller_context.underlying_function.compilation_unit
next_context = caller_context.slither_parser
renaming = caller_context.underlying_function.file_scope.renaming
user_defined_types = caller_context.underlying_function.file_scope.user_defined_types
type_aliases = caller_context.underlying_function.file_scope.type_aliases
structures_direct_access = sl.structures_top_level
all_structuress = [c.structures for c in sl.contracts]
all_structures = [item for sublist in all_structuress for item in sublist]
@ -299,7 +299,7 @@ def parse_type(
functions = list(scope.functions)
renaming = scope.renaming
user_defined_types = scope.user_defined_types
type_aliases = scope.type_aliases
elif isinstance(caller_context, (ContractSolc, FunctionSolc)):
sl = caller_context.compilation_unit
if isinstance(caller_context, FunctionSolc):
@ -329,7 +329,7 @@ def parse_type(
functions = contract.functions + contract.modifiers
renaming = scope.renaming
user_defined_types = scope.user_defined_types
type_aliases = scope.type_aliases
else:
raise ParsingError(f"Incorrect caller context: {type(caller_context)}")
@ -343,8 +343,8 @@ def parse_type(
name = t.name
if name in renaming:
name = renaming[name]
if name in user_defined_types:
return user_defined_types[name]
if name in type_aliases:
return type_aliases[name]
return _find_from_type_name(
name,
functions,
@ -365,9 +365,9 @@ def parse_type(
name = t["typeDescriptions"]["typeString"]
if name in renaming:
name = renaming[name]
if name in user_defined_types:
_add_type_references(user_defined_types[name], t["src"], sl)
return user_defined_types[name]
if name in type_aliases:
_add_type_references(type_aliases[name], t["src"], sl)
return type_aliases[name]
type_found = _find_from_type_name(
name,
functions,
@ -386,9 +386,9 @@ def parse_type(
name = t["attributes"][type_name_key]
if name in renaming:
name = renaming[name]
if name in user_defined_types:
_add_type_references(user_defined_types[name], t["src"], sl)
return user_defined_types[name]
if name in type_aliases:
_add_type_references(type_aliases[name], t["src"], sl)
return type_aliases[name]
type_found = _find_from_type_name(
name,
functions,
@ -407,8 +407,8 @@ def parse_type(
name = t["name"]
if name in renaming:
name = renaming[name]
if name in user_defined_types:
return user_defined_types[name]
if name in type_aliases:
return type_aliases[name]
type_found = _find_from_type_name(
name,
functions,

@ -0,0 +1,348 @@
"""
CK Metrics are a suite of six software metrics proposed by Chidamber and Kemerer in 1994.
These metrics are used to measure the complexity of a class.
https://en.wikipedia.org/wiki/Programming_complexity
- Response For a Class (RFC) is a metric that measures the number of unique method calls within a class.
- Number of Children (NOC) is a metric that measures the number of children a class has.
- Depth of Inheritance Tree (DIT) is a metric that measures the number of parent classes a class has.
- Coupling Between Object Classes (CBO) is a metric that measures the number of classes a class is coupled to.
Not implemented:
- Lack of Cohesion of Methods (LCOM) is a metric that measures the lack of cohesion in methods.
- Weighted Methods per Class (WMC) is a metric that measures the complexity of a class.
During the calculation of the metrics above, there are a number of other intermediate metrics that are calculated.
These are also included in the output:
- State variables: total number of state variables
- Constants: total number of constants
- Immutables: total number of immutables
- Public: total number of public functions
- External: total number of external functions
- Internal: total number of internal functions
- Private: total number of private functions
- Mutating: total number of state mutating functions
- View: total number of view functions
- Pure: total number of pure functions
- External mutating: total number of external mutating functions
- No auth or onlyOwner: total number of functions without auth or onlyOwner modifiers
- No modifiers: total number of functions without modifiers
- Ext calls: total number of external calls
"""
from collections import OrderedDict
from typing import Tuple, List, Dict
from dataclasses import dataclass, field
from slither.utils.colors import bold
from slither.core.declarations import Contract
from slither.utils.myprettytable import make_pretty_table, MyPrettyTable
from slither.utils.martin import MartinMetrics
from slither.slithir.operations.high_level_call import HighLevelCall
# Utility functions
def compute_dit(contract: Contract, depth: int = 0) -> int:
"""
Recursively compute the depth of inheritance tree (DIT) of a contract
Args:
contract(core.declarations.contract.Contract): contract to compute DIT for
depth(int): current depth of the contract
Returns:
int: depth of the contract
"""
if not contract.inheritance:
return depth
max_dit = depth
for inherited_contract in contract.inheritance:
dit = compute_dit(inherited_contract, depth + 1)
max_dit = max(max_dit, dit)
return max_dit
def has_auth(func) -> bool:
"""
Check if a function has no auth or only_owner modifiers
Args:
func(core.declarations.function.Function): function to check
Returns:
bool True if it does have auth or only_owner modifiers
"""
for modifier in func.modifiers:
if "auth" in modifier.name or "only_owner" in modifier.name:
return True
return False
# Utility classes for calculating CK metrics
@dataclass
# pylint: disable=too-many-instance-attributes
class CKContractMetrics:
"""Class to hold the CK metrics for a single contract."""
contract: Contract
# Used to calculate CBO - should be passed in as a constructor arg
martin_metrics: Dict
# Used to calculate NOC
dependents: Dict
state_variables: int = 0
constants: int = 0
immutables: int = 0
public: int = 0
external: int = 0
internal: int = 0
private: int = 0
mutating: int = 0
view: int = 0
pure: int = 0
external_mutating: int = 0
no_auth_or_only_owner: int = 0
no_modifiers: int = 0
ext_calls: int = 0
rfc: int = 0
noc: int = 0
dit: int = 0
cbo: int = 0
def __post_init__(self) -> None:
if not hasattr(self.contract, "functions"):
return
self.count_variables()
self.noc = len(self.dependents[self.contract.name])
self.dit = compute_dit(self.contract)
self.cbo = (
self.martin_metrics[self.contract.name].ca + self.martin_metrics[self.contract.name].ce
)
self.calculate_metrics()
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
def calculate_metrics(self) -> None:
"""Calculate the metrics for a contract"""
rfc = self.public # initialize with public getter count
for func in self.contract.functions:
if func.name == "constructor":
continue
pure = func.pure
view = not pure and func.view
mutating = not pure and not view
external = func.visibility == "external"
public = func.visibility == "public"
internal = func.visibility == "internal"
private = func.visibility == "private"
external_public_mutating = external or public and mutating
external_no_auth = external_public_mutating and not has_auth(func)
external_no_modifiers = external_public_mutating and len(func.modifiers) == 0
if external or public:
rfc += 1
high_level_calls = [
ir for node in func.nodes for ir in node.irs_ssa if isinstance(ir, HighLevelCall)
]
# convert irs to string with target function and contract name
external_calls = []
for high_level_call in high_level_calls:
if isinstance(high_level_call.destination, Contract):
destination_contract = high_level_call.destination.name
elif isinstance(high_level_call.destination, str):
destination_contract = high_level_call.destination
elif not hasattr(high_level_call.destination, "type"):
continue
elif isinstance(high_level_call.destination.type, Contract):
destination_contract = high_level_call.destination.type.name
elif isinstance(high_level_call.destination.type, str):
destination_contract = high_level_call.destination.type
elif not hasattr(high_level_call.destination.type, "type"):
continue
elif isinstance(high_level_call.destination.type.type, Contract):
destination_contract = high_level_call.destination.type.type.name
elif isinstance(high_level_call.destination.type.type, str):
destination_contract = high_level_call.destination.type.type
else:
continue
external_calls.append(f"{high_level_call.function_name}{destination_contract}")
rfc += len(set(external_calls))
self.public += public
self.external += external
self.internal += internal
self.private += private
self.mutating += mutating
self.view += view
self.pure += pure
self.external_mutating += external_public_mutating
self.no_auth_or_only_owner += external_no_auth
self.no_modifiers += external_no_modifiers
self.ext_calls += len(external_calls)
self.rfc = rfc
def count_variables(self) -> None:
"""Count the number of variables in a contract"""
state_variable_count = 0
constant_count = 0
immutable_count = 0
public_getter_count = 0
for variable in self.contract.variables:
if variable.is_constant:
constant_count += 1
elif variable.is_immutable:
immutable_count += 1
else:
state_variable_count += 1
if variable.visibility == "Public":
public_getter_count += 1
self.state_variables = state_variable_count
self.constants = constant_count
self.immutables = immutable_count
# initialize RFC with public getter count
# self.public is used count public functions not public variables
self.rfc = public_getter_count
def to_dict(self) -> Dict[str, float]:
"""Return the metrics as a dictionary."""
return OrderedDict(
{
"State variables": self.state_variables,
"Constants": self.constants,
"Immutables": self.immutables,
"Public": self.public,
"External": self.external,
"Internal": self.internal,
"Private": self.private,
"Mutating": self.mutating,
"View": self.view,
"Pure": self.pure,
"External mutating": self.external_mutating,
"No auth or onlyOwner": self.no_auth_or_only_owner,
"No modifiers": self.no_modifiers,
"Ext calls": self.ext_calls,
"RFC": self.rfc,
"NOC": self.noc,
"DIT": self.dit,
"CBO": self.cbo,
}
)
@dataclass
class SectionInfo:
"""Class to hold the information for a section of the report."""
title: str
pretty_table: MyPrettyTable
txt: str
@dataclass
# pylint: disable=too-many-instance-attributes
class CKMetrics:
"""Class to hold the CK metrics for all contracts. Contains methods useful for reporting.
There are 5 sections in the report:
1. Variable count by type (state, constant, immutable)
2. Function count by visibility (public, external, internal, private)
3. Function count by mutability (mutating, view, pure)
4. External mutating function count by modifier (external mutating, no auth or onlyOwner, no modifiers)
5. CK metrics (RFC, NOC, DIT, CBO)
"""
contracts: List[Contract] = field(default_factory=list)
contract_metrics: OrderedDict = field(default_factory=OrderedDict)
title: str = "CK complexity metrics"
full_text: str = ""
auxiliary1: SectionInfo = field(default=SectionInfo)
auxiliary2: SectionInfo = field(default=SectionInfo)
auxiliary3: SectionInfo = field(default=SectionInfo)
auxiliary4: SectionInfo = field(default=SectionInfo)
core: SectionInfo = field(default=SectionInfo)
AUXILIARY1_KEYS = (
"State variables",
"Constants",
"Immutables",
)
AUXILIARY2_KEYS = (
"Public",
"External",
"Internal",
"Private",
)
AUXILIARY3_KEYS = (
"Mutating",
"View",
"Pure",
)
AUXILIARY4_KEYS = (
"External mutating",
"No auth or onlyOwner",
"No modifiers",
)
CORE_KEYS = (
"Ext calls",
"RFC",
"NOC",
"DIT",
"CBO",
)
SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = (
("Variables", "auxiliary1", AUXILIARY1_KEYS),
("Function visibility", "auxiliary2", AUXILIARY2_KEYS),
("State mutability", "auxiliary3", AUXILIARY3_KEYS),
("External mutating functions", "auxiliary4", AUXILIARY4_KEYS),
("Core", "core", CORE_KEYS),
)
def __post_init__(self) -> None:
martin_metrics = MartinMetrics(self.contracts).contract_metrics
dependents = {
inherited.name: {
contract.name
for contract in self.contracts
if inherited.name in contract.inheritance
}
for inherited in self.contracts
}
for contract in self.contracts:
self.contract_metrics[contract.name] = CKContractMetrics(
contract=contract, martin_metrics=martin_metrics, dependents=dependents
)
# Create the table and text for each section.
data = {
contract.name: self.contract_metrics[contract.name].to_dict()
for contract in self.contracts
}
subtitle = ""
# Update each section
for (title, attr, keys) in self.SECTIONS:
if attr == "core":
# Special handling for core section
totals_enabled = False
subtitle += bold("RFC: Response For a Class\n")
subtitle += bold("NOC: Number of Children\n")
subtitle += bold("DIT: Depth of Inheritance Tree\n")
subtitle += bold("CBO: Coupling Between Object Classes\n")
else:
totals_enabled = True
subtitle = ""
pretty_table = make_pretty_table(["Contract", *keys], data, totals=totals_enabled)
section_title = f"{self.title} ({title})"
txt = f"\n\n{section_title}:\n{subtitle}{pretty_table}\n"
self.full_text += txt
setattr(
self,
attr,
SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt),
)

@ -0,0 +1,202 @@
from typing import Union
from slither.core import variables
from slither.core.declarations import (
SolidityVariable,
SolidityVariableComposed,
Structure,
Enum,
Contract,
)
from slither.core import solidity_types
from slither.slithir import operations
from slither.slithir import variables as SlitherIRVariable
# pylint: disable=too-many-branches
def ntype(_type: Union[solidity_types.Type, str]) -> str:
if isinstance(_type, solidity_types.ElementaryType):
_type = str(_type)
elif isinstance(_type, solidity_types.ArrayType):
if isinstance(_type.type, solidity_types.ElementaryType):
_type = str(_type)
else:
_type = "user_defined_array"
elif isinstance(_type, Structure):
_type = str(_type)
elif isinstance(_type, Enum):
_type = str(_type)
elif isinstance(_type, solidity_types.MappingType):
_type = str(_type)
elif isinstance(_type, solidity_types.UserDefinedType):
if isinstance(_type.type, Contract):
_type = f"contract({_type.type.name})"
elif isinstance(_type.type, Structure):
_type = f"struct({_type.type.name})"
elif isinstance(_type.type, Enum):
_type = f"enum({_type.type.name})"
else:
_type = str(_type)
_type = _type.replace(" memory", "")
_type = _type.replace(" storage ref", "")
if "struct" in _type:
return "struct"
if "enum" in _type:
return "enum"
if "tuple" in _type:
return "tuple"
if "contract" in _type:
return "contract"
if "mapping" in _type:
return "mapping"
return _type.replace(" ", "_")
# pylint: disable=too-many-branches
def encode_var_for_compare(var: Union[variables.Variable, SolidityVariable]) -> str:
# variables
if isinstance(var, SlitherIRVariable.Constant):
return f"constant({ntype(var.type)},{var.value})"
if isinstance(var, SolidityVariableComposed):
return f"solidity_variable_composed({var.name})"
if isinstance(var, SolidityVariable):
return f"solidity_variable{var.name}"
if isinstance(var, SlitherIRVariable.TemporaryVariable):
return "temporary_variable"
if isinstance(var, SlitherIRVariable.ReferenceVariable):
return f"reference({ntype(var.type)})"
if isinstance(var, variables.LocalVariable):
return f"local_solc_variable({ntype(var.type)},{var.location})"
if isinstance(var, variables.StateVariable):
if not (var.is_constant or var.is_immutable):
try:
slot, _ = var.contract.compilation_unit.storage_layout_of(var.contract, var)
except KeyError:
slot = var.name
else:
slot = var.name
return f"state_solc_variable({ntype(var.type)},{slot})"
if isinstance(var, variables.LocalVariableInitFromTuple):
return "local_variable_init_tuple"
if isinstance(var, SlitherIRVariable.TupleVariable):
return "tuple_variable"
# default
return ""
# pylint: disable=too-many-branches
def encode_ir_for_upgradeability_compare(ir: operations.Operation) -> str:
# operations
if isinstance(ir, operations.Assignment):
return f"({encode_var_for_compare(ir.lvalue)}):=({encode_var_for_compare(ir.rvalue)})"
if isinstance(ir, operations.Index):
return f"index({ntype(ir.variable_right.type)})"
if isinstance(ir, operations.Member):
return "member" # .format(ntype(ir._type))
if isinstance(ir, operations.Length):
return "length"
if isinstance(ir, operations.Binary):
return f"binary({encode_var_for_compare(ir.variable_left)}{ir.type}{encode_var_for_compare(ir.variable_right)})"
if isinstance(ir, operations.Unary):
return f"unary({str(ir.type)})"
if isinstance(ir, operations.Condition):
return f"condition({encode_var_for_compare(ir.value)})"
if isinstance(ir, operations.NewStructure):
return "new_structure"
if isinstance(ir, operations.NewContract):
return "new_contract"
if isinstance(ir, operations.NewArray):
return f"new_array({ntype(ir.array_type)})"
if isinstance(ir, operations.NewElementaryType):
return f"new_elementary({ntype(ir.type)})"
if isinstance(ir, operations.Delete):
return f"delete({encode_var_for_compare(ir.lvalue)},{encode_var_for_compare(ir.variable)})"
if isinstance(ir, operations.SolidityCall):
return f"solidity_call({ir.function.full_name})"
if isinstance(ir, operations.InternalCall):
return f"internal_call({ntype(ir.type_call)})"
if isinstance(ir, operations.EventCall): # is this useful?
return "event"
if isinstance(ir, operations.LibraryCall):
return "library_call"
if isinstance(ir, operations.InternalDynamicCall):
return "internal_dynamic_call"
if isinstance(ir, operations.HighLevelCall): # TODO: improve
return "high_level_call"
if isinstance(ir, operations.LowLevelCall): # TODO: improve
return "low_level_call"
if isinstance(ir, operations.TypeConversion):
return f"type_conversion({ntype(ir.type)})"
if isinstance(ir, operations.Return): # this can be improved using values
return "return" # .format(ntype(ir.type))
if isinstance(ir, operations.Transfer):
return f"transfer({encode_var_for_compare(ir.call_value)})"
if isinstance(ir, operations.Send):
return f"send({encode_var_for_compare(ir.call_value)})"
if isinstance(ir, operations.Unpack): # TODO: improve
return "unpack"
if isinstance(ir, operations.InitArray): # TODO: improve
return "init_array"
# default
return ""
def encode_ir_for_halstead(ir: operations.Operation) -> str:
# operations
if isinstance(ir, operations.Assignment):
return "assignment"
if isinstance(ir, operations.Index):
return "index"
if isinstance(ir, operations.Member):
return "member" # .format(ntype(ir._type))
if isinstance(ir, operations.Length):
return "length"
if isinstance(ir, operations.Binary):
return f"binary({str(ir.type)})"
if isinstance(ir, operations.Unary):
return f"unary({str(ir.type)})"
if isinstance(ir, operations.Condition):
return f"condition({encode_var_for_compare(ir.value)})"
if isinstance(ir, operations.NewStructure):
return "new_structure"
if isinstance(ir, operations.NewContract):
return "new_contract"
if isinstance(ir, operations.NewArray):
return f"new_array({ntype(ir.array_type)})"
if isinstance(ir, operations.NewElementaryType):
return f"new_elementary({ntype(ir.type)})"
if isinstance(ir, operations.Delete):
return "delete"
if isinstance(ir, operations.SolidityCall):
return f"solidity_call({ir.function.full_name})"
if isinstance(ir, operations.InternalCall):
return f"internal_call({ntype(ir.type_call)})"
if isinstance(ir, operations.EventCall): # is this useful?
return "event"
if isinstance(ir, operations.LibraryCall):
return "library_call"
if isinstance(ir, operations.InternalDynamicCall):
return "internal_dynamic_call"
if isinstance(ir, operations.HighLevelCall): # TODO: improve
return "high_level_call"
if isinstance(ir, operations.LowLevelCall): # TODO: improve
return "low_level_call"
if isinstance(ir, operations.TypeConversion):
return f"type_conversion({ntype(ir.type)})"
if isinstance(ir, operations.Return): # this can be improved using values
return "return" # .format(ntype(ir.type))
if isinstance(ir, operations.Transfer):
return "transfer"
if isinstance(ir, operations.Send):
return "send"
if isinstance(ir, operations.Unpack): # TODO: improve
return "unpack"
if isinstance(ir, operations.InitArray): # TODO: improve
return "init_array"
# default
raise NotImplementedError(f"encode_ir_for_halstead: {ir}")

@ -147,7 +147,7 @@ class SplitTernaryExpression:
for next_expr in expression.expressions:
# TODO: can we get rid of `NoneType` expressions in `TupleExpression`?
# montyly: this might happen with unnamed tuple (ex: (,,,) = f()), but it needs to be checked
if next_expr:
if next_expr is not None:
if self.conditional_not_ahead(
next_expr, true_expression, false_expression, f_expressions
@ -158,6 +158,9 @@ class SplitTernaryExpression:
true_expression.expressions[-1],
false_expression.expressions[-1],
)
else:
true_expression.expressions.append(None)
false_expression.expressions.append(None)
def convert_index_access(
self, next_expr: IndexAccess, true_expression: Expression, false_expression: Expression

@ -0,0 +1,233 @@
"""
Halstead complexity metrics
https://en.wikipedia.org/wiki/Halstead_complexity_measures
12 metrics based on the number of unique operators and operands:
Core metrics:
n1 = the number of distinct operators
n2 = the number of distinct operands
N1 = the total number of operators
N2 = the total number of operands
Extended metrics1:
n = n1 + n2 # Program vocabulary
N = N1 + N2 # Program length
S = n1 * log2(n1) + n2 * log2(n2) # Estimated program length
V = N * log2(n) # Volume
Extended metrics2:
D = (n1 / 2) * (N2 / n2) # Difficulty
E = D * V # Effort
T = E / 18 seconds # Time required to program
B = (E^(2/3)) / 3000 # Number of delivered bugs
"""
import math
from collections import OrderedDict
from dataclasses import dataclass, field
from typing import Tuple, List, Dict
from slither.core.declarations import Contract
from slither.slithir.variables.temporary import TemporaryVariable
from slither.utils.encoding import encode_ir_for_halstead
from slither.utils.myprettytable import make_pretty_table, MyPrettyTable
# pylint: disable=too-many-branches
@dataclass
# pylint: disable=too-many-instance-attributes
class HalsteadContractMetrics:
"""Class to hold the Halstead metrics for a single contract."""
contract: Contract
all_operators: List[str] = field(default_factory=list)
all_operands: List[str] = field(default_factory=list)
n1: int = 0
n2: int = 0
N1: int = 0
N2: int = 0
n: int = 0
N: int = 0
S: float = 0
V: float = 0
D: float = 0
E: float = 0
T: float = 0
B: float = 0
def __post_init__(self) -> None:
"""Operators and operands can be passed in as constructor args to avoid computing
them based on the contract. Useful for computing metrics for ALL_CONTRACTS"""
if len(self.all_operators) == 0:
if not hasattr(self.contract, "functions"):
return
self.populate_operators_and_operands()
if len(self.all_operators) > 0:
self.compute_metrics()
def to_dict(self) -> Dict[str, float]:
"""Return the metrics as a dictionary."""
return OrderedDict(
{
"Total Operators": self.N1,
"Unique Operators": self.n1,
"Total Operands": self.N2,
"Unique Operands": self.n2,
"Vocabulary": str(self.n1 + self.n2),
"Program Length": str(self.N1 + self.N2),
"Estimated Length": f"{self.S:.0f}",
"Volume": f"{self.V:.0f}",
"Difficulty": f"{self.D:.0f}",
"Effort": f"{self.E:.0f}",
"Time": f"{self.T:.0f}",
"Estimated Bugs": f"{self.B:.3f}",
}
)
def populate_operators_and_operands(self) -> None:
"""Populate the operators and operands lists."""
operators = []
operands = []
for func in self.contract.functions:
for node in func.nodes:
for operation in node.irs:
# use operation.expression.type to get the unique operator type
encoded_operator = encode_ir_for_halstead(operation)
operators.append(encoded_operator)
# use operation.used to get the operands of the operation ignoring the temporary variables
operands.extend(
[op for op in operation.used if not isinstance(op, TemporaryVariable)]
)
self.all_operators.extend(operators)
self.all_operands.extend(operands)
def compute_metrics(self, all_operators=None, all_operands=None) -> None:
"""Compute the Halstead metrics."""
if all_operators is None:
all_operators = self.all_operators
all_operands = self.all_operands
# core metrics
self.n1 = len(set(all_operators))
self.n2 = len(set(all_operands))
self.N1 = len(all_operators)
self.N2 = len(all_operands)
if any(number <= 0 for number in [self.n1, self.n2, self.N1, self.N2]):
raise ValueError("n1 and n2 must be greater than 0")
# extended metrics 1
self.n = self.n1 + self.n2
self.N = self.N1 + self.N2
self.S = self.n1 * math.log2(self.n1) + self.n2 * math.log2(self.n2)
self.V = self.N * math.log2(self.n)
# extended metrics 2
self.D = (self.n1 / 2) * (self.N2 / self.n2)
self.E = self.D * self.V
self.T = self.E / 18
self.B = (self.E ** (2 / 3)) / 3000
@dataclass
class SectionInfo:
"""Class to hold the information for a section of the report."""
title: str
pretty_table: MyPrettyTable
txt: str
@dataclass
# pylint: disable=too-many-instance-attributes
class HalsteadMetrics:
"""Class to hold the Halstead metrics for all contracts. Contains methods useful for reporting.
There are 3 sections in the report:
1. Core metrics (n1, n2, N1, N2)
2. Extended metrics 1 (n, N, S, V)
3. Extended metrics 2 (D, E, T, B)
"""
contracts: List[Contract] = field(default_factory=list)
contract_metrics: OrderedDict = field(default_factory=OrderedDict)
title: str = "Halstead complexity metrics"
full_text: str = ""
core: SectionInfo = field(default=SectionInfo)
extended1: SectionInfo = field(default=SectionInfo)
extended2: SectionInfo = field(default=SectionInfo)
CORE_KEYS = (
"Total Operators",
"Unique Operators",
"Total Operands",
"Unique Operands",
)
EXTENDED1_KEYS = (
"Vocabulary",
"Program Length",
"Estimated Length",
"Volume",
)
EXTENDED2_KEYS = (
"Difficulty",
"Effort",
"Time",
"Estimated Bugs",
)
SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = (
("Core", "core", CORE_KEYS),
("Extended 1/2", "extended1", EXTENDED1_KEYS),
("Extended 2/2", "extended2", EXTENDED2_KEYS),
)
def __post_init__(self) -> None:
# Compute the metrics for each contract and for all contracts.
self.update_contract_metrics()
self.add_all_contracts_metrics()
self.update_reporting_sections()
def update_contract_metrics(self) -> None:
for contract in self.contracts:
self.contract_metrics[contract.name] = HalsteadContractMetrics(contract=contract)
def add_all_contracts_metrics(self) -> None:
# If there are more than 1 contract, compute the metrics for all contracts.
if len(self.contracts) <= 1:
return
all_operators = [
operator
for contract in self.contracts
for operator in self.contract_metrics[contract.name].all_operators
]
all_operands = [
operand
for contract in self.contracts
for operand in self.contract_metrics[contract.name].all_operands
]
self.contract_metrics["ALL CONTRACTS"] = HalsteadContractMetrics(
None, all_operators=all_operators, all_operands=all_operands
)
def update_reporting_sections(self) -> None:
# Create the table and text for each section.
data = {
contract.name: self.contract_metrics[contract.name].to_dict()
for contract in self.contracts
}
for (title, attr, keys) in self.SECTIONS:
pretty_table = make_pretty_table(["Contract", *keys], data, False)
section_title = f"{self.title} ({title})"
txt = f"\n\n{section_title}:\n{pretty_table}\n"
self.full_text += txt
setattr(
self,
attr,
SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt),
)

@ -0,0 +1,157 @@
"""
Robert "Uncle Bob" Martin - Agile software metrics
https://en.wikipedia.org/wiki/Software_package_metrics
Efferent Coupling (Ce): Number of contracts that the contract depends on
Afferent Coupling (Ca): Number of contracts that depend on a contract
Instability (I): Ratio of efferent coupling to total coupling (Ce / (Ce + Ca))
Abstractness (A): Number of abstract contracts / total number of contracts
Distance from the Main Sequence (D): abs(A + I - 1)
"""
from typing import Tuple, List, Dict
from dataclasses import dataclass, field
from collections import OrderedDict
from slither.slithir.operations.high_level_call import HighLevelCall
from slither.core.declarations import Contract
from slither.utils.myprettytable import make_pretty_table, MyPrettyTable
@dataclass
class MartinContractMetrics:
contract: Contract
ca: int
ce: int
abstractness: float
i: float = 0.0
d: float = 0.0
def __post_init__(self) -> None:
if self.ce + self.ca > 0:
self.i = float(self.ce / (self.ce + self.ca))
self.d = float(abs(self.i - self.abstractness))
def to_dict(self) -> Dict:
return {
"Dependents": self.ca,
"Dependencies": self.ce,
"Instability": f"{self.i:.2f}",
"Distance from main sequence": f"{self.d:.2f}",
}
@dataclass
class SectionInfo:
"""Class to hold the information for a section of the report."""
title: str
pretty_table: MyPrettyTable
txt: str
@dataclass
class MartinMetrics:
contracts: List[Contract] = field(default_factory=list)
abstractness: float = 0.0
contract_metrics: OrderedDict = field(default_factory=OrderedDict)
title: str = "Martin complexity metrics"
full_text: str = ""
core: SectionInfo = field(default=SectionInfo)
CORE_KEYS = (
"Dependents",
"Dependencies",
"Instability",
"Distance from main sequence",
)
SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = (("Core", "core", CORE_KEYS),)
def __post_init__(self) -> None:
self.update_abstractness()
self.update_coupling()
self.update_reporting_sections()
def update_reporting_sections(self) -> None:
# Create the table and text for each section.
data = {
contract.name: self.contract_metrics[contract.name].to_dict()
for contract in self.contracts
}
for (title, attr, keys) in self.SECTIONS:
pretty_table = make_pretty_table(["Contract", *keys], data, False)
section_title = f"{self.title} ({title})"
txt = f"\n\n{section_title}:\n"
txt = "Martin agile software metrics\n"
txt += "Efferent Coupling (Ce) - Number of contracts that a contract depends on\n"
txt += "Afferent Coupling (Ca) - Number of contracts that depend on the contract\n"
txt += (
"Instability (I) - Ratio of efferent coupling to total coupling (Ce / (Ce + Ca))\n"
)
txt += "Abstractness (A) - Number of abstract contracts / total number of contracts\n"
txt += "Distance from the Main Sequence (D) - abs(A + I - 1)\n"
txt += "\n"
txt += f"Abstractness (overall): {round(self.abstractness, 2)}\n"
txt += f"{pretty_table}\n"
self.full_text += txt
setattr(
self,
attr,
SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt),
)
def update_abstractness(self) -> None:
abstract_contract_count = 0
for c in self.contracts:
if not c.is_fully_implemented:
abstract_contract_count += 1
self.abstractness = float(abstract_contract_count / len(self.contracts))
# pylint: disable=too-many-branches
def update_coupling(self) -> None:
dependencies = {}
for contract in self.contracts:
external_calls = []
for func in contract.functions:
high_level_calls = [
ir
for node in func.nodes
for ir in node.irs_ssa
if isinstance(ir, HighLevelCall)
]
# convert irs to string with target function and contract name
# Get the target contract name for each high level call
new_external_calls = []
for high_level_call in high_level_calls:
if isinstance(high_level_call.destination, Contract):
new_external_call = high_level_call.destination.name
elif isinstance(high_level_call.destination, str):
new_external_call = high_level_call.destination
elif not hasattr(high_level_call.destination, "type"):
continue
elif isinstance(high_level_call.destination.type, Contract):
new_external_call = high_level_call.destination.type.name
elif isinstance(high_level_call.destination.type, str):
new_external_call = high_level_call.destination.type
elif not hasattr(high_level_call.destination.type, "type"):
continue
elif isinstance(high_level_call.destination.type.type, Contract):
new_external_call = high_level_call.destination.type.type.name
elif isinstance(high_level_call.destination.type.type, str):
new_external_call = high_level_call.destination.type.type
else:
continue
new_external_calls.append(new_external_call)
external_calls.extend(new_external_calls)
dependencies[contract.name] = set(external_calls)
dependents = {}
for contract, deps in dependencies.items():
for dep in deps:
if dep not in dependents:
dependents[dep] = set()
dependents[dep].add(contract)
for contract in self.contracts:
ce = len(dependencies.get(contract.name, []))
ca = len(dependents.get(contract.name, []))
self.contract_metrics[contract.name] = MartinContractMetrics(
contract, ca, ce, self.abstractness
)

@ -4,9 +4,17 @@ from prettytable.colortable import ColorTable, Themes
class MyPrettyTable:
def __init__(self, field_names: List[str]):
def __init__(self, field_names: List[str], pretty_align: bool = True): # TODO: True by default?
self._field_names = field_names
self._rows: List = []
self._options: Dict = {}
if pretty_align:
self._options["set_alignment"] = []
self._options["set_alignment"] += [(field_names[0], "l")]
for field_name in field_names[1:]:
self._options["set_alignment"] += [(field_name, "r")]
else:
self._options["set_alignment"] = []
def add_row(self, row: List[Union[str, List[str]]]) -> None:
self._rows.append(row)
@ -15,6 +23,9 @@ class MyPrettyTable:
table = ColorTable(self._field_names, theme=Themes.OCEAN)
for row in self._rows:
table.add_row(row)
if len(self._options["set_alignment"]):
for column_header, value in self._options["set_alignment"]:
table.align[column_header] = value
return table
def to_json(self) -> Dict:
@ -22,3 +33,30 @@ class MyPrettyTable:
def __str__(self) -> str:
return str(self.to_pretty_table())
# UTILITY FUNCTIONS
def make_pretty_table(
headers: list, body: dict, totals: bool = False, total_header="TOTAL"
) -> MyPrettyTable:
"""
Converts a dict to a MyPrettyTable. Dict keys are the row headers.
Args:
headers: str[] of column names
body: dict of row headers with a dict of the values
totals: bool optional add Totals row
total_header: str optional if totals is set to True this will override the default "TOTAL" header
Returns:
MyPrettyTable
"""
table = MyPrettyTable(headers)
for row in body:
table_row = [row] + [body[row][key] for key in headers[1:]]
table.add_row(table_row)
if totals:
table.add_row(
[total_header] + [sum([body[row][key] for row in body]) for key in headers[1:]]
)
return table

@ -1,66 +1,28 @@
from typing import Optional, Tuple, List, Union
from typing import Optional, Tuple, List
from slither.analyses.data_dependency.data_dependency import get_dependencies
from slither.core.cfg.node import Node, NodeType
from slither.core.declarations import (
Contract,
Structure,
Enum,
SolidityVariableComposed,
SolidityVariable,
Function,
)
from slither.core.solidity_types import (
Type,
ElementaryType,
ArrayType,
MappingType,
UserDefinedType,
)
from slither.core.variables.local_variable import LocalVariable
from slither.core.variables.local_variable_init_from_tuple import LocalVariableInitFromTuple
from slither.core.variables.state_variable import StateVariable
from slither.analyses.data_dependency.data_dependency import get_dependencies
from slither.core.variables.variable import Variable
from slither.core.expressions import (
Literal,
Identifier,
CallExpression,
AssignmentOperation,
)
from slither.core.cfg.node import Node, NodeType
from slither.core.solidity_types import (
ElementaryType,
)
from slither.core.variables.local_variable import LocalVariable
from slither.core.variables.state_variable import StateVariable
from slither.core.variables.variable import Variable
from slither.slithir.operations import (
Operation,
Assignment,
Index,
Member,
Length,
Binary,
Unary,
Condition,
NewArray,
NewStructure,
NewContract,
NewElementaryType,
SolidityCall,
Delete,
EventCall,
LibraryCall,
InternalDynamicCall,
HighLevelCall,
LowLevelCall,
TypeConversion,
Return,
Transfer,
Send,
Unpack,
InitArray,
InternalCall,
)
from slither.slithir.variables import (
TemporaryVariable,
TupleVariable,
Constant,
ReferenceVariable,
)
from slither.tools.read_storage.read_storage import SlotInfo, SlitherReadStorage
from slither.utils.encoding import encode_ir_for_upgradeability_compare
class TaintedExternalContract:
@ -385,144 +347,13 @@ def is_function_modified(f1: Function, f2: Function) -> bool:
if len(node_f1.irs) != len(node_f2.irs):
return True
for i, ir in enumerate(node_f1.irs):
if encode_ir_for_compare(ir) != encode_ir_for_compare(node_f2.irs[i]):
if encode_ir_for_upgradeability_compare(ir) != encode_ir_for_upgradeability_compare(
node_f2.irs[i]
):
return True
return False
# pylint: disable=too-many-branches
def ntype(_type: Union[Type, str]) -> str:
if isinstance(_type, ElementaryType):
_type = str(_type)
elif isinstance(_type, ArrayType):
if isinstance(_type.type, ElementaryType):
_type = str(_type)
else:
_type = "user_defined_array"
elif isinstance(_type, Structure):
_type = str(_type)
elif isinstance(_type, Enum):
_type = str(_type)
elif isinstance(_type, MappingType):
_type = str(_type)
elif isinstance(_type, UserDefinedType):
if isinstance(_type.type, Contract):
_type = f"contract({_type.type.name})"
elif isinstance(_type.type, Structure):
_type = f"struct({_type.type.name})"
elif isinstance(_type.type, Enum):
_type = f"enum({_type.type.name})"
else:
_type = str(_type)
_type = _type.replace(" memory", "")
_type = _type.replace(" storage ref", "")
if "struct" in _type:
return "struct"
if "enum" in _type:
return "enum"
if "tuple" in _type:
return "tuple"
if "contract" in _type:
return "contract"
if "mapping" in _type:
return "mapping"
return _type.replace(" ", "_")
# pylint: disable=too-many-branches
def encode_ir_for_compare(ir: Operation) -> str:
# operations
if isinstance(ir, Assignment):
return f"({encode_var_for_compare(ir.lvalue)}):=({encode_var_for_compare(ir.rvalue)})"
if isinstance(ir, Index):
return f"index({ntype(ir.variable_right.type)})"
if isinstance(ir, Member):
return "member" # .format(ntype(ir._type))
if isinstance(ir, Length):
return "length"
if isinstance(ir, Binary):
return f"binary({encode_var_for_compare(ir.variable_left)}{ir.type}{encode_var_for_compare(ir.variable_right)})"
if isinstance(ir, Unary):
return f"unary({str(ir.type)})"
if isinstance(ir, Condition):
return f"condition({encode_var_for_compare(ir.value)})"
if isinstance(ir, NewStructure):
return "new_structure"
if isinstance(ir, NewContract):
return "new_contract"
if isinstance(ir, NewArray):
return f"new_array({ntype(ir.array_type)})"
if isinstance(ir, NewElementaryType):
return f"new_elementary({ntype(ir.type)})"
if isinstance(ir, Delete):
return f"delete({encode_var_for_compare(ir.lvalue)},{encode_var_for_compare(ir.variable)})"
if isinstance(ir, SolidityCall):
return f"solidity_call({ir.function.full_name})"
if isinstance(ir, InternalCall):
return f"internal_call({ntype(ir.type_call)})"
if isinstance(ir, EventCall): # is this useful?
return "event"
if isinstance(ir, LibraryCall):
return "library_call"
if isinstance(ir, InternalDynamicCall):
return "internal_dynamic_call"
if isinstance(ir, HighLevelCall): # TODO: improve
return "high_level_call"
if isinstance(ir, LowLevelCall): # TODO: improve
return "low_level_call"
if isinstance(ir, TypeConversion):
return f"type_conversion({ntype(ir.type)})"
if isinstance(ir, Return): # this can be improved using values
return "return" # .format(ntype(ir.type))
if isinstance(ir, Transfer):
return f"transfer({encode_var_for_compare(ir.call_value)})"
if isinstance(ir, Send):
return f"send({encode_var_for_compare(ir.call_value)})"
if isinstance(ir, Unpack): # TODO: improve
return "unpack"
if isinstance(ir, InitArray): # TODO: improve
return "init_array"
# default
return ""
# pylint: disable=too-many-branches
def encode_var_for_compare(var: Variable) -> str:
# variables
if isinstance(var, Constant):
return f"constant({ntype(var.type)},{var.value})"
if isinstance(var, SolidityVariableComposed):
return f"solidity_variable_composed({var.name})"
if isinstance(var, SolidityVariable):
return f"solidity_variable{var.name}"
if isinstance(var, TemporaryVariable):
return "temporary_variable"
if isinstance(var, ReferenceVariable):
return f"reference({ntype(var.type)})"
if isinstance(var, LocalVariable):
return f"local_solc_variable({ntype(var.type)},{var.location})"
if isinstance(var, StateVariable):
if not (var.is_constant or var.is_immutable):
try:
slot, _ = var.contract.compilation_unit.storage_layout_of(var.contract, var)
except KeyError:
slot = var.name
else:
slot = var.name
return f"state_solc_variable({ntype(var.type)},{slot})"
if isinstance(var, LocalVariableInitFromTuple):
return "local_variable_init_tuple"
if isinstance(var, TupleVariable):
return "tuple_variable"
# default
return ""
def get_proxy_implementation_slot(proxy: Contract) -> Optional[SlotInfo]:
"""
Gets information about the storage slot where a proxy's implementation address is stored.

@ -14,6 +14,7 @@ from slither.core.declarations import (
Enum,
SolidityImportPlaceHolder,
Import,
Structure
)
from slither.core.expressions import (
AssignmentOperation,
@ -53,6 +54,7 @@ from slither.slithir.operations import (
Member,
TypeConversion,
Unary,
UnaryType,
Unpack,
Return,
SolidityCall,
@ -112,6 +114,13 @@ _binary_to_binary = {
BinaryOperationType.OROR: BinaryType.OROR,
}
_unary_to_unary = {
UnaryOperationType.BANG: UnaryType.BANG,
UnaryOperationType.TILD: UnaryType.TILD,
}
_signed_to_unsigned = {
BinaryOperationType.DIVISION_SIGNED: BinaryType.DIVISION,
BinaryOperationType.MODULO_SIGNED: BinaryType.MODULO,
@ -173,6 +182,7 @@ class ExpressionToSlithIR(ExpressionVisitor):
def result(self) -> List[Operation]:
return self._result
# pylint: disable=too-many-branches,too-many-statements
def _post_assignement_operation(self, expression: AssignmentOperation) -> None:
left = get(expression.expression_left)
right = get(expression.expression_right)
@ -236,6 +246,35 @@ class ExpressionToSlithIR(ExpressionVisitor):
operation.set_expression(expression)
self._result.append(operation)
set_val(expression, left)
elif (
isinstance(left.type, UserDefinedType)
and isinstance(left.type.type, Structure)
and isinstance(right, TupleVariable)
):
# This will result in a `NewStructure` operation where
# each field is assigned the value unpacked from the tuple
# (see `slither.vyper_parsing.type_parsing.parse_type`)
args = []
for idx, elem in enumerate(left.type.type.elems.values()):
temp = TemporaryVariable(self._node)
temp.type = elem.type
args.append(temp)
operation = Unpack(temp, right, idx)
operation.set_expression(expression)
self._result.append(operation)
for arg in args:
op = Argument(arg)
op.set_expression(expression)
self._result.append(op)
operation = TmpCall(
left.type.type, len(left.type.type.elems), left, left.type.type.name
)
operation.set_expression(expression)
self._result.append(operation)
else:
operation = convert_assignment(
left, right, expression.type, expression.expression_return_type
@ -307,7 +346,9 @@ class ExpressionToSlithIR(ExpressionVisitor):
val = TupleVariable(self._node)
else:
val = TemporaryVariable(self._node)
internal_call = InternalCall(called, len(args), val, expression.type_call)
internal_call = InternalCall(
called, len(args), val, expression.type_call, names=expression.names
)
internal_call.set_expression(expression)
self._result.append(internal_call)
set_val(expression, val)
@ -376,7 +417,9 @@ class ExpressionToSlithIR(ExpressionVisitor):
else:
val = TemporaryVariable(self._node)
message_call = TmpCall(called, len(args), val, expression.type_call)
message_call = TmpCall(
called, len(args), val, expression.type_call, names=expression.names
)
message_call.set_expression(expression)
# Gas/value are only accessible here if the syntax {gas: , value: }
# Is used over .gas().value()
@ -420,6 +463,21 @@ class ExpressionToSlithIR(ExpressionVisitor):
set_val(expression, t)
return
val = ReferenceVariable(self._node)
if (
isinstance(left, LocalVariable)
and isinstance(left.type, UserDefinedType)
and isinstance(left.type.type, Structure)
):
# We rewrite the index access to a tuple variable as
# an access to its field i.e. the 0th element is the field "_0"
# (see `slither.vyper_parsing.type_parsing.parse_type`)
operation = Member(left, Constant("_" + str(right)), val)
operation.set_expression(expression)
self._result.append(operation)
set_val(expression, val)
return
# access to anonymous array
# such as [0,1][x]
if isinstance(left, list):
@ -429,6 +487,7 @@ class ExpressionToSlithIR(ExpressionVisitor):
operation = InitArray(init_array_right, init_array_val)
operation.set_expression(expression)
self._result.append(operation)
operation = Index(val, left, right)
operation.set_expression(expression)
self._result.append(operation)
@ -446,6 +505,7 @@ class ExpressionToSlithIR(ExpressionVisitor):
# Look for type(X).max / min
# Because we looked at the AST structure, we need to look into the nested expression
# Hopefully this is always on a direct sub field, and there is no weird construction
# pylint: disable=too-many-nested-blocks
if isinstance(expression.expression, CallExpression) and expression.member_name in [
"min",
"max",
@ -465,10 +525,22 @@ class ExpressionToSlithIR(ExpressionVisitor):
constant_type = type_found
else:
# type(enum).max/min
assert isinstance(type_expression_found, Identifier)
type_found_in_expression = type_expression_found.value
assert isinstance(type_found_in_expression, (EnumContract, EnumTopLevel))
type_found = UserDefinedType(type_found_in_expression)
# Case when enum is in another contract e.g. type(C.E).max
if isinstance(type_expression_found, MemberAccess):
contract = type_expression_found.expression.value
assert isinstance(contract, Contract)
for enum in contract.enums:
if enum.name == type_expression_found.member_name:
type_found_in_expression = enum
type_found = UserDefinedType(enum)
break
else:
assert isinstance(type_expression_found, Identifier)
type_found_in_expression = type_expression_found.value
assert isinstance(
type_found_in_expression, (EnumContract, EnumTopLevel)
)
type_found = UserDefinedType(type_found_in_expression)
constant_type = None
min_value = type_found_in_expression.min
max_value = type_found_in_expression.max
@ -519,13 +591,17 @@ class ExpressionToSlithIR(ExpressionVisitor):
# contract A { type MyInt is int}
# contract B { function f() public{ A.MyInt test = A.MyInt.wrap(1);}}
# The logic is handled by _post_call_expression
if expression.member_name in expr.file_scope.user_defined_types:
set_val(expression, expr.file_scope.user_defined_types[expression.member_name])
if expression.member_name in expr.file_scope.type_aliases:
set_val(expression, expr.file_scope.type_aliases[expression.member_name])
return
# Lookup errors referred to as member of contract e.g. Test.myError.selector
if expression.member_name in expr.custom_errors_as_dict:
set_val(expression, expr.custom_errors_as_dict[expression.member_name])
return
# Lookup enums when in a different contract e.g. C.E
if str(expression) in expr.enums_as_dict:
set_val(expression, expr.enums_as_dict[str(expression)])
return
if isinstance(expr, (SolidityImportPlaceHolder, Import)):
scope = (
@ -639,7 +715,7 @@ class ExpressionToSlithIR(ExpressionVisitor):
expr = get(expression.expression)
val = TemporaryVariable(self._node)
expression_type = expression.type
assert isinstance(expression_type, (TypeAlias, UserDefinedType, ElementaryType))
assert isinstance(expression_type, (TypeAlias, UserDefinedType, ElementaryType, ArrayType))
operation = TypeConversion(val, expr, expression_type)
val.set_type(expression.type)
operation.set_expression(expression)
@ -652,7 +728,7 @@ class ExpressionToSlithIR(ExpressionVisitor):
operation: Operation
if expression.type in [UnaryOperationType.BANG, UnaryOperationType.TILD]:
lvalue = TemporaryVariable(self._node)
operation = Unary(lvalue, value, expression.type)
operation = Unary(lvalue, value, _unary_to_unary[expression.type])
operation.set_expression(expression)
self._result.append(operation)
set_val(expression, lvalue)

@ -0,0 +1,466 @@
from typing import Dict, Callable, List
from slither.vyper_parsing.ast.types import (
ASTNode,
Module,
ImportFrom,
EventDef,
AnnAssign,
Name,
Call,
StructDef,
VariableDecl,
Subscript,
Index,
Hex,
Int,
Str,
Tuple,
FunctionDef,
Assign,
Raise,
Attribute,
Assert,
Keyword,
Arguments,
Arg,
UnaryOp,
BinOp,
Expr,
Log,
Return,
VyDict,
VyList,
NameConstant,
If,
Compare,
For,
Break,
Continue,
Pass,
InterfaceDef,
EnumDef,
Bytes,
AugAssign,
BoolOp,
)
class ParsingError(Exception):
pass
def _extract_base_props(raw: Dict) -> Dict:
return {
"src": raw["src"],
"node_id": raw["node_id"],
}
def _extract_decl_props(raw: Dict) -> Dict:
return {
"doc_string": parse_doc_str(raw["doc_string"]) if raw["doc_string"] else None,
**_extract_base_props(raw),
}
def parse_module(raw: Dict) -> Module:
nodes_parsed: List[ASTNode] = []
for node in raw["body"]:
nodes_parsed.append(parse(node))
return Module(name=raw["name"], body=nodes_parsed, **_extract_decl_props(raw))
def parse_import_from(raw: Dict) -> ImportFrom:
return ImportFrom(
module=raw["module"],
name=raw["name"],
alias=raw["alias"],
**_extract_base_props(raw),
)
def parse_event_def(raw: Dict) -> EventDef:
body_parsed: List[ASTNode] = []
for node in raw["body"]:
body_parsed.append(parse(node))
return EventDef(
name=raw["name"],
body=body_parsed,
*_extract_base_props(raw),
)
def parse_ann_assign(raw: Dict) -> AnnAssign:
return AnnAssign(
target=parse(raw["target"]),
annotation=parse(raw["annotation"]),
value=parse(raw["value"]) if raw["value"] else None,
**_extract_base_props(raw),
)
def parse_name(raw: Dict) -> Name:
return Name(
id=raw["id"],
**_extract_base_props(raw),
)
def parse_call(raw: Dict) -> Call:
return Call(
func=parse(raw["func"]),
args=[parse(arg) for arg in raw["args"]],
keyword=parse(raw["keyword"]) if raw["keyword"] else None,
keywords=[parse(keyword) for keyword in raw["keywords"]],
**_extract_base_props(raw),
)
def parse_struct_def(raw: Dict) -> StructDef:
body_parsed: List[ASTNode] = []
for node in raw["body"]:
body_parsed.append(parse(node))
return StructDef(
name=raw["name"],
body=body_parsed,
**_extract_base_props(raw),
)
def parse_variable_decl(raw: Dict) -> VariableDecl:
return VariableDecl(
annotation=parse(raw["annotation"]),
value=parse(raw["value"]) if raw["value"] else None,
target=parse(raw["target"]),
is_constant=raw["is_constant"],
is_immutable=raw["is_immutable"],
is_public=raw["is_public"],
**_extract_base_props(raw),
)
def parse_subscript(raw: Dict) -> Subscript:
return Subscript(
value=parse(raw["value"]),
slice=parse(raw["slice"]),
**_extract_base_props(raw),
)
def parse_index(raw: Dict) -> Index:
return Index(value=parse(raw["value"]), **_extract_base_props(raw))
def parse_bytes(raw: Dict) -> Bytes:
return Bytes(value=raw["value"], **_extract_base_props(raw))
def parse_hex(raw: Dict) -> Hex:
return Hex(value=raw["value"], **_extract_base_props(raw))
def parse_int(raw: Dict) -> Int:
return Int(value=raw["value"], **_extract_base_props(raw))
def parse_str(raw: Dict) -> Str:
return Str(value=raw["value"], **_extract_base_props(raw))
def parse_tuple(raw: Dict) -> ASTNode:
return Tuple(elements=[parse(elem) for elem in raw["elements"]], **_extract_base_props(raw))
def parse_function_def(raw: Dict) -> FunctionDef:
body_parsed: List[ASTNode] = []
for node in raw["body"]:
body_parsed.append(parse(node))
decorators_parsed: List[ASTNode] = []
for node in raw["decorator_list"]:
decorators_parsed.append(parse(node))
return FunctionDef(
name=raw["name"],
args=parse_arguments(raw["args"]),
returns=parse(raw["returns"]) if raw["returns"] else None,
body=body_parsed,
pos=raw["pos"],
decorators=decorators_parsed,
**_extract_decl_props(raw),
)
def parse_assign(raw: Dict) -> Assign:
return Assign(
target=parse(raw["target"]),
value=parse(raw["value"]),
**_extract_base_props(raw),
)
def parse_attribute(raw: Dict) -> Attribute:
return Attribute(
value=parse(raw["value"]),
attr=raw["attr"],
**_extract_base_props(raw),
)
def parse_arguments(raw: Dict) -> Arguments:
return Arguments(
args=[parse_arg(arg) for arg in raw["args"]],
default=parse(raw["default"]) if raw["default"] else None,
defaults=[parse(x) for x in raw["defaults"]],
**_extract_base_props(raw),
)
def parse_arg(raw: Dict) -> Arg:
return Arg(arg=raw["arg"], annotation=parse(raw["annotation"]), **_extract_base_props(raw))
def parse_assert(raw: Dict) -> Assert:
return Assert(
test=parse(raw["test"]),
msg=parse(raw["msg"]) if raw["msg"] else None,
**_extract_base_props(raw),
)
def parse_raise(raw: Dict) -> Raise:
return Raise(exc=parse(raw["exc"]) if raw["exc"] else None, **_extract_base_props(raw))
def parse_expr(raw: Dict) -> Expr:
return Expr(value=parse(raw["value"]), **_extract_base_props(raw))
# This is done for convenience so we can call `UnaryOperationType.get_type` during expression parsing.
unop_ast_type_to_op_symbol = {"Not": "!", "USub": "-"}
def parse_unary_op(raw: Dict) -> UnaryOp:
unop_str = unop_ast_type_to_op_symbol[raw["op"]["ast_type"]]
return UnaryOp(op=unop_str, operand=parse(raw["operand"]), **_extract_base_props(raw))
# This is done for convenience so we can call `BinaryOperationType.get_type` during expression parsing.
binop_ast_type_to_op_symbol = {
"Add": "+",
"Mult": "*",
"Sub": "-",
"Div": "/",
"Pow": "**",
"Mod": "%",
"BitAnd": "&",
"BitOr": "|",
"Shr": "<<",
"Shl": ">>",
"NotEq": "!=",
"Eq": "==",
"LtE": "<=",
"GtE": ">=",
"Lt": "<",
"Gt": ">",
"In": "In",
"NotIn": "NotIn",
}
def parse_bin_op(raw: Dict) -> BinOp:
arith_op_str = binop_ast_type_to_op_symbol[raw["op"]["ast_type"]]
return BinOp(
left=parse(raw["left"]),
op=arith_op_str,
right=parse(raw["right"]),
**_extract_base_props(raw),
)
def parse_compare(raw: Dict) -> Compare:
logical_op_str = binop_ast_type_to_op_symbol[raw["op"]["ast_type"]]
return Compare(
left=parse(raw["left"]),
op=logical_op_str,
right=parse(raw["right"]),
**_extract_base_props(raw),
)
def parse_keyword(raw: Dict) -> Keyword:
return Keyword(arg=raw["arg"], value=parse(raw["value"]), **_extract_base_props(raw))
def parse_log(raw: Dict) -> Log:
return Log(value=parse(raw["value"]), **_extract_base_props(raw))
def parse_return(raw: Dict) -> Return:
return Return(value=parse(raw["value"]) if raw["value"] else None, **_extract_base_props(raw))
def parse_dict(raw: Dict) -> ASTNode:
return VyDict(
keys=[parse(x) for x in raw["keys"]],
values=[parse(x) for x in raw["values"]],
**_extract_base_props(raw),
)
def parse_list(raw: Dict) -> VyList:
return VyList(elements=[parse(x) for x in raw["elements"]], **_extract_base_props(raw))
def parse_name_constant(raw: Dict) -> NameConstant:
return NameConstant(value=raw["value"], **_extract_base_props(raw))
def parse_doc_str(raw: Dict) -> str:
assert isinstance(raw["value"], str)
return raw["value"]
def parse_if(raw: Dict) -> ASTNode:
return If(
test=parse(raw["test"]),
body=[parse(x) for x in raw["body"]],
orelse=[parse(x) for x in raw["orelse"]],
**_extract_base_props(raw),
)
def parse_for(raw: Dict) -> For:
return For(
target=parse(raw["target"]),
iter=parse(raw["iter"]),
body=[parse(x) for x in raw["body"]],
**_extract_base_props(raw),
)
def parse_break(raw: Dict) -> Break:
return Break(**_extract_base_props(raw))
def parse_continue(raw: Dict) -> Continue:
return Continue(**_extract_base_props(raw))
def parse_pass(raw: Dict) -> Pass:
return Pass(
**_extract_base_props(raw),
)
def parse_interface_def(raw: Dict) -> InterfaceDef:
nodes_parsed: List[ASTNode] = []
for node in raw["body"]:
nodes_parsed.append(parse(node))
return InterfaceDef(name=raw["name"], body=nodes_parsed, **_extract_base_props(raw))
def parse_enum_def(raw: Dict) -> EnumDef:
nodes_parsed: List[ASTNode] = []
for node in raw["body"]:
nodes_parsed.append(parse(node))
return EnumDef(name=raw["name"], body=nodes_parsed, **_extract_base_props(raw))
aug_assign_ast_type_to_op_symbol = {
"Add": "+=",
"Mult": "*=",
"Sub": "-=",
"Div": "-=",
"Pow": "**=",
"Mod": "%=",
"BitAnd": "&=",
"BitOr": "|=",
"Shr": "<<=",
"Shl": ">>=",
}
def parse_aug_assign(raw: Dict) -> AugAssign:
op_str = aug_assign_ast_type_to_op_symbol[raw["op"]["ast_type"]]
return AugAssign(
target=parse(raw["target"]),
op=op_str,
value=parse(raw["value"]),
**_extract_base_props(raw),
)
def parse_unsupported(raw: Dict) -> ASTNode:
raise ParsingError("unsupported Vyper node", raw["ast_type"], raw.keys(), raw)
bool_op_ast_type_to_op_symbol = {"And": "&&", "Or": "||"}
def parse_bool_op(raw: Dict) -> BoolOp:
op_str = bool_op_ast_type_to_op_symbol[raw["op"]["ast_type"]]
return BoolOp(op=op_str, values=[parse(x) for x in raw["values"]], **_extract_base_props(raw))
def parse(raw: Dict) -> ASTNode:
try:
return PARSERS.get(raw["ast_type"], parse_unsupported)(raw)
except ParsingError as e:
raise e
except Exception as e:
raise e
# raise ParsingError("failed to parse Vyper node", raw["ast_type"], e, raw.keys(), raw)
PARSERS: Dict[str, Callable[[Dict], ASTNode]] = {
"Module": parse_module,
"ImportFrom": parse_import_from,
"EventDef": parse_event_def,
"AnnAssign": parse_ann_assign,
"Name": parse_name,
"Call": parse_call,
"Pass": parse_pass,
"StructDef": parse_struct_def,
"VariableDecl": parse_variable_decl,
"Subscript": parse_subscript,
"Index": parse_index,
"Hex": parse_hex,
"Int": parse_int,
"Str": parse_str,
"DocStr": parse_doc_str,
"Tuple": parse_tuple,
"FunctionDef": parse_function_def,
"Assign": parse_assign,
"Raise": parse_raise,
"Attribute": parse_attribute,
"Assert": parse_assert,
"keyword": parse_keyword,
"arguments": parse_arguments,
"arg": parse_arg,
"UnaryOp": parse_unary_op,
"BinOp": parse_bin_op,
"Expr": parse_expr,
"Log": parse_log,
"Return": parse_return,
"If": parse_if,
"Dict": parse_dict,
"List": parse_list,
"Compare": parse_compare,
"NameConstant": parse_name_constant,
"For": parse_for,
"Break": parse_break,
"Continue": parse_continue,
"InterfaceDef": parse_interface_def,
"EnumDef": parse_enum_def,
"Bytes": parse_bytes,
"AugAssign": parse_aug_assign,
"BoolOp": parse_bool_op,
}

@ -0,0 +1,262 @@
from __future__ import annotations
from typing import List, Optional, Union
from dataclasses import dataclass
@dataclass
class ASTNode:
src: str
node_id: int
@dataclass
class Definition(ASTNode):
doc_string: Optional[str]
@dataclass
class Module(Definition):
body: List[ASTNode]
name: str
@dataclass
class ImportFrom(ASTNode):
module: str
name: str
alias: Optional[str]
@dataclass
class EventDef(ASTNode):
name: str
body: List[AnnAssign]
@dataclass
class AnnAssign(ASTNode):
target: Name
annotation: Union[Subscript, Name, Call]
value: Optional[ASTNode]
@dataclass
class Name(ASTNode): # type or identifier
id: str
@dataclass
class Call(ASTNode):
func: ASTNode
args: List[ASTNode]
keyword: Optional[ASTNode]
keywords: List[ASTNode]
@dataclass
class Pass(ASTNode):
pass
@dataclass
class StructDef(ASTNode):
name: str
body: List[AnnAssign]
@dataclass
class VariableDecl(ASTNode):
annotation: ASTNode
target: ASTNode
value: Optional[ASTNode]
is_constant: bool
is_immutable: bool
is_public: bool
@dataclass
class Subscript(ASTNode):
value: ASTNode
slice: ASTNode
@dataclass
class Index(ASTNode):
value: ASTNode
@dataclass
class Bytes(ASTNode):
value: bytes
@dataclass
class Hex(ASTNode):
value: str
@dataclass
class Int(ASTNode):
value: int
@dataclass
class Str(ASTNode):
value: str
@dataclass
class VyList(ASTNode):
elements: List[ASTNode]
@dataclass
class VyDict(ASTNode):
keys: List[ASTNode]
values: List[ASTNode]
@dataclass
class Tuple(ASTNode):
elements: List[ASTNode]
@dataclass
class FunctionDef(Definition):
name: str
args: Optional[Arguments]
returns: Optional[List[ASTNode]]
body: List[ASTNode]
decorators: Optional[List[ASTNode]]
pos: Optional[any] # not sure what this is
@dataclass
class Assign(ASTNode):
target: ASTNode
value: ASTNode
@dataclass
class Attribute(ASTNode):
value: ASTNode
attr: str
@dataclass
class Arguments(ASTNode):
args: List[Arg]
default: Optional[ASTNode]
defaults: List[ASTNode]
@dataclass
class Arg(ASTNode):
arg: str
annotation: Optional[ASTNode]
@dataclass
class Assert(ASTNode):
test: ASTNode
msg: Optional[Str]
@dataclass
class Raise(ASTNode):
exc: ASTNode
@dataclass
class Expr(ASTNode):
value: ASTNode
@dataclass
class UnaryOp(ASTNode):
op: ASTNode
operand: ASTNode
@dataclass
class BinOp(ASTNode):
left: ASTNode
op: str
right: ASTNode
@dataclass
class Keyword(ASTNode):
arg: str
value: ASTNode
@dataclass
class Log(ASTNode):
value: ASTNode
@dataclass
class Return(ASTNode):
value: Optional[ASTNode]
@dataclass
class If(ASTNode):
test: ASTNode
body: List[ASTNode]
orelse: List[ASTNode]
@dataclass
class Compare(ASTNode):
left: ASTNode
op: ASTNode
right: ASTNode
@dataclass
class NameConstant(ASTNode):
value: bool
@dataclass
class For(ASTNode):
target: ASTNode
iter: ASTNode
body: List[ASTNode]
@dataclass
class Continue(ASTNode):
pass
@dataclass
class Break(ASTNode):
pass
@dataclass
class InterfaceDef(ASTNode):
name: str
body: List[ASTNode]
@dataclass
class EnumDef(ASTNode):
name: str
body: List[ASTNode]
@dataclass
class AugAssign(ASTNode):
target: ASTNode
op: ASTNode
value: ASTNode
@dataclass
class BoolOp(ASTNode):
op: ASTNode
values: List[ASTNode]

@ -0,0 +1,66 @@
from typing import Optional, Dict
from slither.core.cfg.node import Node
from slither.core.cfg.node import NodeType
from slither.core.expressions.assignment_operation import (
AssignmentOperation,
AssignmentOperationType,
)
from slither.core.expressions.identifier import Identifier
from slither.vyper_parsing.expressions.expression_parsing import parse_expression
from slither.visitors.expression.find_calls import FindCalls
from slither.visitors.expression.read_var import ReadVar
from slither.visitors.expression.write_var import WriteVar
class NodeVyper:
def __init__(self, node: Node) -> None:
self._unparsed_expression: Optional[Dict] = None
self._node = node
@property
def underlying_node(self) -> Node:
return self._node
def add_unparsed_expression(self, expression: Dict) -> None:
assert self._unparsed_expression is None
self._unparsed_expression = expression
def analyze_expressions(self, caller_context) -> None:
if self._node.type == NodeType.VARIABLE and not self._node.expression:
self._node.add_expression(self._node.variable_declaration.expression)
if self._unparsed_expression:
expression = parse_expression(self._unparsed_expression, caller_context)
self._node.add_expression(expression)
self._unparsed_expression = None
if self._node.expression:
if self._node.type == NodeType.VARIABLE:
# Update the expression to be an assignement to the variable
_expression = AssignmentOperation(
Identifier(self._node.variable_declaration),
self._node.expression,
AssignmentOperationType.ASSIGN,
self._node.variable_declaration.type,
)
_expression.set_offset(
self._node.expression.source_mapping, self._node.compilation_unit
)
self._node.add_expression(_expression, bypass_verif_empty=True)
expression = self._node.expression
read_var = ReadVar(expression)
self._node.variables_read_as_expression = read_var.result()
write_var = WriteVar(expression)
self._node.variables_written_as_expression = write_var.result()
find_call = FindCalls(expression)
self._node.calls_as_expression = find_call.result()
self._node.external_calls_as_expressions = [
c for c in self._node.calls_as_expression if not isinstance(c.called, Identifier)
]
self._node.internal_calls_as_expressions = [
c for c in self._node.calls_as_expression if isinstance(c.called, Identifier)
]

@ -0,0 +1,524 @@
from pathlib import Path
from typing import List, TYPE_CHECKING
from slither.vyper_parsing.ast.types import (
Module,
FunctionDef,
EventDef,
EnumDef,
StructDef,
VariableDecl,
ImportFrom,
InterfaceDef,
AnnAssign,
Expr,
Name,
Arguments,
Index,
Subscript,
Int,
Arg,
)
from slither.vyper_parsing.declarations.event import EventVyper
from slither.vyper_parsing.declarations.struct import StructVyper
from slither.vyper_parsing.variables.state_variable import StateVariableVyper
from slither.vyper_parsing.declarations.function import FunctionVyper
from slither.core.declarations.function_contract import FunctionContract
from slither.core.declarations import Contract, StructureContract, EnumContract, Event
from slither.core.variables.state_variable import StateVariable
if TYPE_CHECKING:
from slither.vyper_parsing.vyper_compilation_unit import VyperCompilationUnit
class ContractVyper: # pylint: disable=too-many-instance-attributes
def __init__(
self, slither_parser: "VyperCompilationUnit", contract: Contract, module: Module
) -> None:
self._contract: Contract = contract
self._slither_parser: "VyperCompilationUnit" = slither_parser
self._data = module
# Vyper models only have one contract (aside from interfaces) and the name is the file path
# We use the stem to make it a more user friendly name that is easy to query via canonical name
self._contract.name = Path(module.name).stem
self._contract.id = module.node_id
self._is_analyzed: bool = False
self._enumsNotParsed: List[EnumDef] = []
self._structuresNotParsed: List[StructDef] = []
self._variablesNotParsed: List[VariableDecl] = []
self._eventsNotParsed: List[EventDef] = []
self._functionsNotParsed: List[FunctionDef] = []
self._structures_parser: List[StructVyper] = []
self._variables_parser: List[StateVariableVyper] = []
self._events_parser: List[EventVyper] = []
self._functions_parser: List[FunctionVyper] = []
self._parse_contract_items()
@property
def is_analyzed(self) -> bool:
return self._is_analyzed
def set_is_analyzed(self, is_analyzed: bool) -> None:
self._is_analyzed = is_analyzed
@property
def underlying_contract(self) -> Contract:
return self._contract
def _parse_contract_items(self) -> None:
for node in self._data.body:
if isinstance(node, FunctionDef):
self._functionsNotParsed.append(node)
elif isinstance(node, EventDef):
self._eventsNotParsed.append(node)
elif isinstance(node, VariableDecl):
self._variablesNotParsed.append(node)
elif isinstance(node, EnumDef):
self._enumsNotParsed.append(node)
elif isinstance(node, StructDef):
self._structuresNotParsed.append(node)
elif isinstance(node, ImportFrom):
# TOOD aliases
# We create an `InterfaceDef` sense the compilatuion unit does not contain the actual interface
# https://github.com/vyperlang/vyper/tree/master/vyper/builtins/interfaces
if node.module == "vyper.interfaces":
interfaces = {
"ERC20Detailed": InterfaceDef(
src="-1:-1:-1",
node_id=-1,
name="ERC20Detailed",
body=[
FunctionDef(
src="-1:-1:-1",
node_id=-1,
doc_string=None,
name="name",
args=Arguments(
src="-1:-1:-1",
node_id=-1,
args=[],
default=None,
defaults=[],
),
returns=Subscript(
src="-1:-1:-1",
node_id=-1,
value=Name(src="-1:-1:-1", node_id=-1, id="String"),
slice=Index(
src="-1:-1:-1",
node_id=-1,
value=Int(src="-1:-1:-1", node_id=-1, value=1),
),
),
body=[
Expr(
src="-1:-1:-1",
node_id=-1,
value=Name(src="-1:-1:-1", node_id=-1, id="view"),
)
],
decorators=[],
pos=None,
),
FunctionDef(
src="-1:-1:-1",
node_id=-1,
doc_string=None,
name="symbol",
args=Arguments(
src="-1:-1:-1",
node_id=-1,
args=[],
default=None,
defaults=[],
),
returns=Subscript(
src="-1:-1:-1",
node_id=-1,
value=Name(src="-1:-1:-1", node_id=-1, id="String"),
slice=Index(
src="-1:-1:-1",
node_id=-1,
value=Int(src="-1:-1:-1", node_id=-1, value=1),
),
),
body=[
Expr(
src="-1:-1:-1",
node_id=-1,
value=Name(src="-1:-1:-1", node_id=-1, id="view"),
)
],
decorators=[],
pos=None,
),
FunctionDef(
src="-1:-1:-1",
node_id=-1,
doc_string=None,
name="decimals",
args=Arguments(
src="-1:-1:-1",
node_id=-1,
args=[],
default=None,
defaults=[],
),
returns=Name(src="-1:-1:-1", node_id=-1, id="uint8"),
body=[
Expr(
src="-1:-1:-1",
node_id=-1,
value=Name(src="-1:-1:-1", node_id=-1, id="view"),
)
],
decorators=[],
pos=None,
),
],
),
"ERC20": InterfaceDef(
src="-1:-1:-1",
node_id=1,
name="ERC20",
body=[
FunctionDef(
src="-1:-1:-1",
node_id=2,
doc_string=None,
name="totalSupply",
args=Arguments(
src="-1:-1:-1",
node_id=3,
args=[],
default=None,
defaults=[],
),
returns=Name(src="-1:-1:-1", node_id=7, id="uint256"),
body=[
Expr(
src="-1:-1:-1",
node_id=4,
value=Name(src="-1:-1:-1", node_id=5, id="view"),
)
],
decorators=[],
pos=None,
),
FunctionDef(
src="-1:-1:-1",
node_id=9,
doc_string=None,
name="balanceOf",
args=Arguments(
src="-1:-1:-1",
node_id=10,
args=[
Arg(
src="-1:-1:-1",
node_id=11,
arg="_owner",
annotation=Name(
src="-1:-1:-1", node_id=12, id="address"
),
)
],
default=None,
defaults=[],
),
returns=Name(src="-1:-1:-1", node_id=17, id="uint256"),
body=[
Expr(
src="-1:-1:-1",
node_id=14,
value=Name(src="-1:-1:-1", node_id=15, id="view"),
)
],
decorators=[],
pos=None,
),
FunctionDef(
src="-1:-1:-1",
node_id=19,
doc_string=None,
name="allowance",
args=Arguments(
src="-1:-1:-1",
node_id=20,
args=[
Arg(
src="-1:-1:-1",
node_id=21,
arg="_owner",
annotation=Name(
src="-1:-1:-1", node_id=22, id="address"
),
),
Arg(
src="-1:-1:-1",
node_id=24,
arg="_spender",
annotation=Name(
src="-1:-1:-1", node_id=25, id="address"
),
),
],
default=None,
defaults=[],
),
returns=Name(src="-1:-1:-1", node_id=30, id="uint256"),
body=[
Expr(
src="-1:-1:-1",
node_id=27,
value=Name(src="-1:-1:-1", node_id=28, id="view"),
)
],
decorators=[],
pos=None,
),
FunctionDef(
src="-1:-1:-1",
node_id=32,
doc_string=None,
name="transfer",
args=Arguments(
src="-1:-1:-1",
node_id=33,
args=[
Arg(
src="-1:-1:-1",
node_id=34,
arg="_to",
annotation=Name(
src="-1:-1:-1", node_id=35, id="address"
),
),
Arg(
src="-1:-1:-1",
node_id=37,
arg="_value",
annotation=Name(
src="-1:-1:-1", node_id=38, id="uint256"
),
),
],
default=None,
defaults=[],
),
returns=Name(src="-1:-1:-1", node_id=43, id="bool"),
body=[
Expr(
src="-1:-1:-1",
node_id=40,
value=Name(src="-1:-1:-1", node_id=41, id="nonpayable"),
)
],
decorators=[],
pos=None,
),
FunctionDef(
src="-1:-1:-1",
node_id=45,
doc_string=None,
name="transferFrom",
args=Arguments(
src="-1:-1:-1",
node_id=46,
args=[
Arg(
src="-1:-1:-1",
node_id=47,
arg="_from",
annotation=Name(
src="-1:-1:-1", node_id=48, id="address"
),
),
Arg(
src="-1:-1:-1",
node_id=50,
arg="_to",
annotation=Name(
src="-1:-1:-1", node_id=51, id="address"
),
),
Arg(
src="-1:-1:-1",
node_id=53,
arg="_value",
annotation=Name(
src="-1:-1:-1", node_id=54, id="uint256"
),
),
],
default=None,
defaults=[],
),
returns=Name(src="-1:-1:-1", node_id=59, id="bool"),
body=[
Expr(
src="-1:-1:-1",
node_id=56,
value=Name(src="-1:-1:-1", node_id=57, id="nonpayable"),
)
],
decorators=[],
pos=None,
),
FunctionDef(
src="-1:-1:-1",
node_id=61,
doc_string=None,
name="approve",
args=Arguments(
src="-1:-1:-1",
node_id=62,
args=[
Arg(
src="-1:-1:-1",
node_id=63,
arg="_spender",
annotation=Name(
src="-1:-1:-1", node_id=64, id="address"
),
),
Arg(
src="-1:-1:-1",
node_id=66,
arg="_value",
annotation=Name(
src="-1:-1:-1", node_id=67, id="uint256"
),
),
],
default=None,
defaults=[],
),
returns=Name(src="-1:-1:-1", node_id=72, id="bool"),
body=[
Expr(
src="-1:-1:-1",
node_id=69,
value=Name(src="-1:-1:-1", node_id=70, id="nonpayable"),
)
],
decorators=[],
pos=None,
),
],
),
"ERC165": [],
"ERC721": [],
"ERC4626": [],
}
self._data.body.append(interfaces[node.name])
elif isinstance(node, InterfaceDef):
# This needs to be done lazily as interfaces can refer to constant state variables
contract = Contract(self._contract.compilation_unit, self._contract.file_scope)
contract.set_offset(node.src, self._contract.compilation_unit)
contract.is_interface = True
contract_parser = ContractVyper(self._slither_parser, contract, node)
self._contract.file_scope.contracts[contract.name] = contract
# pylint: disable=protected-access
self._slither_parser._underlying_contract_to_parser[contract] = contract_parser
elif isinstance(node, AnnAssign): # implements: ERC20
pass # TODO
else:
raise ValueError("Unknown contract node: ", node)
def parse_enums(self) -> None:
for enum in self._enumsNotParsed:
name = enum.name
canonicalName = self._contract.name + "." + enum.name
values = [x.value.id for x in enum.body]
new_enum = EnumContract(name, canonicalName, values)
new_enum.set_contract(self._contract)
new_enum.set_offset(enum.src, self._contract.compilation_unit)
self._contract.enums_as_dict[name] = new_enum # TODO solidity using canonicalName
self._enumsNotParsed = []
def parse_structs(self) -> None:
for struct in self._structuresNotParsed:
st = StructureContract(self._contract.compilation_unit)
st.set_contract(self._contract)
st.set_offset(struct.src, self._contract.compilation_unit)
st_parser = StructVyper(st, struct)
self._contract.structures_as_dict[st.name] = st
self._structures_parser.append(st_parser)
# Interfaces can refer to struct defs
self._contract.file_scope.structures[st.name] = st
self._structuresNotParsed = []
def parse_state_variables(self) -> None:
for varNotParsed in self._variablesNotParsed:
var = StateVariable()
var.set_contract(self._contract)
var.set_offset(varNotParsed.src, self._contract.compilation_unit)
var_parser = StateVariableVyper(var, varNotParsed)
self._variables_parser.append(var_parser)
assert var.name
self._contract.variables_as_dict[var.name] = var
self._contract.add_variables_ordered([var])
# Interfaces can refer to constants
self._contract.file_scope.variables[var.name] = var
self._variablesNotParsed = []
def parse_events(self) -> None:
for event_to_parse in self._eventsNotParsed:
event = Event()
event.set_contract(self._contract)
event.set_offset(event_to_parse.src, self._contract.compilation_unit)
event_parser = EventVyper(event, event_to_parse)
self._events_parser.append(event_parser)
self._contract.events_as_dict[event.full_name] = event
def parse_functions(self) -> None:
for function in self._functionsNotParsed:
func = FunctionContract(self._contract.compilation_unit)
func.set_offset(function.src, self._contract.compilation_unit)
func.set_contract(self._contract)
func.set_contract_declarer(self._contract)
func_parser = FunctionVyper(func, function, self)
self._contract.add_function(func)
self._contract.compilation_unit.add_function(func)
self._functions_parser.append(func_parser)
self._functionsNotParsed = []
def analyze_state_variables(self):
# Struct defs can refer to constant state variables
for var_parser in self._variables_parser:
var_parser.analyze(self._contract)
def analyze(self) -> None:
for struct_parser in self._structures_parser:
struct_parser.analyze(self._contract)
for event_parser in self._events_parser:
event_parser.analyze(self._contract)
for function in self._functions_parser:
function.analyze_params()
for function in self._functions_parser:
function.analyze_content()
def __hash__(self) -> int:
return self._contract.id

@ -0,0 +1,39 @@
"""
Event module
"""
from slither.core.variables.event_variable import EventVariable
from slither.vyper_parsing.variables.event_variable import EventVariableVyper
from slither.core.declarations.event import Event
from slither.vyper_parsing.ast.types import AnnAssign, Pass
from slither.vyper_parsing.ast.types import EventDef
class EventVyper: # pylint: disable=too-few-public-methods
"""
Event class
"""
def __init__(self, event: Event, event_def: EventDef) -> None:
self._event = event
self._event.name = event_def.name
self._elemsNotParsed = event_def.body
def analyze(self, contract) -> None:
for elem_to_parse in self._elemsNotParsed:
if not isinstance(elem_to_parse, AnnAssign):
assert isinstance(elem_to_parse, Pass)
continue
elem = EventVariable()
elem.set_offset(elem_to_parse.src, self._event.contract.compilation_unit)
event_parser = EventVariableVyper(elem, elem_to_parse)
event_parser.analyze(contract)
self._event.elems.append(elem)
self._elemsNotParsed = []

@ -0,0 +1,563 @@
from typing import Dict, Union, List, TYPE_CHECKING
from slither.core.cfg.node import NodeType, link_nodes, Node
from slither.core.cfg.scope import Scope
from slither.core.declarations.function import (
Function,
FunctionType,
)
from slither.core.declarations.function import ModifierStatements
from slither.core.declarations.modifier import Modifier
from slither.core.source_mapping.source_mapping import Source
from slither.core.variables.local_variable import LocalVariable
from slither.vyper_parsing.cfg.node import NodeVyper
from slither.solc_parsing.exceptions import ParsingError
from slither.vyper_parsing.variables.local_variable import LocalVariableVyper
from slither.vyper_parsing.ast.types import (
Int,
Call,
Attribute,
Name,
Tuple as TupleVyper,
ASTNode,
AnnAssign,
FunctionDef,
Return,
Assert,
Compare,
Log,
Subscript,
If,
Pass,
Assign,
AugAssign,
Raise,
Expr,
For,
Index,
Arg,
Arguments,
Continue,
Break,
)
if TYPE_CHECKING:
from slither.core.compilation_unit import SlitherCompilationUnit
from slither.vyper_parsing.declarations.contract import ContractVyper
def link_underlying_nodes(node1: NodeVyper, node2: NodeVyper):
link_nodes(node1.underlying_node, node2.underlying_node)
class FunctionVyper: # pylint: disable=too-many-instance-attributes
def __init__(
self,
function: Function,
function_data: FunctionDef,
contract_parser: "ContractVyper",
) -> None:
self._function = function
self._function.name = function_data.name
self._function.id = function_data.node_id
self._functionNotParsed = function_data
self._decoratorNotParsed = None
self._local_variables_parser: List[LocalVariableVyper] = []
self._variables_renamed = []
self._contract_parser = contract_parser
self._node_to_NodeVyper: Dict[Node, NodeVyper] = {}
for decorator in function_data.decorators:
if isinstance(decorator, Call):
# TODO handle multiple
self._decoratorNotParsed = decorator
elif isinstance(decorator, Name):
if decorator.id in ["external", "public", "internal"]:
self._function.visibility = decorator.id
elif decorator.id == "view":
self._function.view = True
elif decorator.id == "pure":
self._function.pure = True
elif decorator.id == "payable":
self._function.payable = True
elif decorator.id == "nonpayable":
self._function.payable = False
else:
raise ValueError(f"Unknown decorator {decorator.id}")
# Interfaces do not have decorators and are external
if self._function._visibility is None:
self._function.visibility = "external"
self._params_was_analyzed = False
self._content_was_analyzed = False
self._counter_scope_local_variables = 0
if function_data.doc_string is not None:
function.has_documentation = True
self._analyze_function_type()
@property
def underlying_function(self) -> Function:
return self._function
@property
def compilation_unit(self) -> "SlitherCompilationUnit":
return self._function.compilation_unit
###################################################################################
###################################################################################
# region Variables
###################################################################################
###################################################################################
@property
def variables_renamed(
self,
) -> Dict[int, LocalVariableVyper]:
return self._variables_renamed
def _add_local_variable(self, local_var_parser: LocalVariableVyper) -> None:
# Ensure variables name are unique for SSA conversion
# This should not apply to actual Vyper variables currently
# but is necessary if we have nested loops where we've created artificial variables e.g. counter_var
if local_var_parser.underlying_variable.name:
known_variables = [v.name for v in self._function.variables]
while local_var_parser.underlying_variable.name in known_variables:
local_var_parser.underlying_variable.name += (
f"_scope_{self._counter_scope_local_variables}"
)
self._counter_scope_local_variables += 1
known_variables = [v.name for v in self._function.variables]
# TODO no reference ID
# if local_var_parser.reference_id is not None:
# self._variables_renamed[local_var_parser.reference_id] = local_var_parser
self._function.variables_as_dict[
local_var_parser.underlying_variable.name
] = local_var_parser.underlying_variable
self._local_variables_parser.append(local_var_parser)
# endregion
###################################################################################
###################################################################################
# region Analyses
###################################################################################
###################################################################################
@property
def function_not_parsed(self) -> Dict:
return self._functionNotParsed
def _analyze_function_type(self) -> None:
if self._function.name == "__init__":
self._function.function_type = FunctionType.CONSTRUCTOR
elif self._function.name == "__default__":
self._function.function_type = FunctionType.FALLBACK
else:
self._function.function_type = FunctionType.NORMAL
def analyze_params(self) -> None:
if self._params_was_analyzed:
return
self._params_was_analyzed = True
params = self._functionNotParsed.args
returns = self._functionNotParsed.returns
if params:
self._parse_params(params)
if returns:
self._parse_returns(returns)
def analyze_content(self) -> None:
if self._content_was_analyzed:
return
self._content_was_analyzed = True
body = self._functionNotParsed.body
if body and not isinstance(body[0], Pass):
self._function.is_implemented = True
self._function.is_empty = False
self._parse_cfg(body)
else:
self._function.is_implemented = False
self._function.is_empty = True
for local_var_parser in self._local_variables_parser:
local_var_parser.analyze(self._function)
for node_parser in self._node_to_NodeVyper.values():
node_parser.analyze_expressions(self._function)
self._analyze_decorator()
def _analyze_decorator(self) -> None:
if not self._decoratorNotParsed:
return
decorator = self._decoratorNotParsed
if decorator.args:
name = f"{decorator.func.id}({decorator.args[0].value})"
else:
name = decorator.func.id
contract = self._contract_parser.underlying_contract
compilation_unit = self._contract_parser.underlying_contract.compilation_unit
modifier = Modifier(compilation_unit)
modifier.name = name
modifier.set_offset(decorator.src, compilation_unit)
modifier.set_contract(contract)
modifier.set_contract_declarer(contract)
latest_entry_point = self._function.entry_point
self._function.add_modifier(
ModifierStatements(
modifier=modifier,
entry_point=latest_entry_point,
nodes=[latest_entry_point],
)
)
# endregion
###################################################################################
###################################################################################
# region Nodes
###################################################################################
###################################################################################
def _new_node(
self, node_type: NodeType, src: Union[str, Source], scope: Union[Scope, "Function"]
) -> NodeVyper:
node = self._function.new_node(node_type, src, scope)
node_parser = NodeVyper(node)
self._node_to_NodeVyper[node] = node_parser
return node_parser
# endregion
###################################################################################
###################################################################################
# region Parsing function
###################################################################################
###################################################################################
# pylint: disable=too-many-branches,too-many-statements,protected-access,too-many-locals
def _parse_cfg(self, cfg: List[ASTNode]) -> None:
entry_node = self._new_node(NodeType.ENTRYPOINT, "-1:-1:-1", self.underlying_function)
self._function.entry_point = entry_node.underlying_node
scope = Scope(True, False, self.underlying_function)
def parse_statement(
curr_node: NodeVyper,
expr: ASTNode,
continue_destination=None,
break_destination=None,
) -> NodeVyper:
if isinstance(expr, AnnAssign):
local_var = LocalVariable()
local_var.set_function(self._function)
local_var.set_offset(expr.src, self._function.compilation_unit)
local_var_parser = LocalVariableVyper(local_var, expr)
self._add_local_variable(local_var_parser)
new_node = self._new_node(NodeType.VARIABLE, expr.src, scope)
if expr.value is not None:
local_var.initialized = True
new_node.add_unparsed_expression(expr.value)
new_node.underlying_node.add_variable_declaration(local_var)
link_underlying_nodes(curr_node, new_node)
curr_node = new_node
elif isinstance(expr, (AugAssign, Assign)):
new_node = self._new_node(NodeType.EXPRESSION, expr.src, scope)
new_node.add_unparsed_expression(expr)
link_underlying_nodes(curr_node, new_node)
curr_node = new_node
elif isinstance(expr, Expr):
# TODO This is a workaround to handle Vyper putting payable/view in the function body... https://github.com/vyperlang/vyper/issues/3578
if not isinstance(expr.value, Name):
new_node = self._new_node(NodeType.EXPRESSION, expr.src, scope)
new_node.add_unparsed_expression(expr.value)
link_underlying_nodes(curr_node, new_node)
curr_node = new_node
elif isinstance(expr, For):
node_startLoop = self._new_node(NodeType.STARTLOOP, expr.src, scope)
node_endLoop = self._new_node(NodeType.ENDLOOP, expr.src, scope)
link_underlying_nodes(curr_node, node_startLoop)
local_var = LocalVariable()
local_var.set_function(self._function)
local_var.set_offset(expr.src, self._function.compilation_unit)
counter_var = AnnAssign(
expr.target.src,
expr.target.node_id,
target=Name("-1:-1:-1", -1, "counter_var"),
annotation=Name("-1:-1:-1", -1, "uint256"),
value=Int("-1:-1:-1", -1, 0),
)
local_var_parser = LocalVariableVyper(local_var, counter_var)
self._add_local_variable(local_var_parser)
counter_node = self._new_node(NodeType.VARIABLE, expr.src, scope)
local_var.initialized = True
counter_node.add_unparsed_expression(counter_var.value)
counter_node.underlying_node.add_variable_declaration(local_var)
link_underlying_nodes(node_startLoop, counter_node)
node_condition = None
if isinstance(expr.iter, (Attribute, Name)):
# HACK
# The loop variable is not annotated so we infer its type by looking at the type of the iterator
if isinstance(expr.iter, Attribute): # state variable
iter_expr = expr.iter
loop_iterator = list(
filter(
lambda x: x._variable.name == iter_expr.attr,
self._contract_parser._variables_parser,
)
)[0]
else: # local variable
iter_expr = expr.iter
loop_iterator = list(
filter(
lambda x: x._variable.name == iter_expr.id,
self._local_variables_parser,
)
)[0]
# TODO use expr.src instead of -1:-1:1?
cond_expr = Compare(
"-1:-1:-1",
-1,
left=Name("-1:-1:-1", -1, "counter_var"),
op="<=",
right=Call(
"-1:-1:-1",
-1,
func=Name("-1:-1:-1", -1, "len"),
args=[iter_expr],
keywords=[],
keyword=None,
),
)
node_condition = self._new_node(NodeType.IFLOOP, expr.src, scope)
node_condition.add_unparsed_expression(cond_expr)
if loop_iterator._elem_to_parse.value.id == "DynArray":
loop_var_annotation = loop_iterator._elem_to_parse.slice.value.elements[0]
else:
loop_var_annotation = loop_iterator._elem_to_parse.value
value = Subscript(
"-1:-1:-1",
-1,
value=Name("-1:-1:-1", -1, loop_iterator._variable.name),
slice=Index("-1:-1:-1", -1, value=Name("-1:-1:-1", -1, "counter_var")),
)
loop_var = AnnAssign(
expr.target.src,
expr.target.node_id,
target=expr.target,
annotation=loop_var_annotation,
value=value,
)
elif isinstance(expr.iter, Call): # range
range_val = expr.iter.args[0]
cond_expr = Compare(
"-1:-1:-1",
-1,
left=Name("-1:-1:-1", -1, "counter_var"),
op="<=",
right=range_val,
)
node_condition = self._new_node(NodeType.IFLOOP, expr.src, scope)
node_condition.add_unparsed_expression(cond_expr)
loop_var = AnnAssign(
expr.target.src,
expr.target.node_id,
target=expr.target,
annotation=Name("-1:-1:-1", -1, "uint256"),
value=Name("-1:-1:-1", -1, "counter_var"),
)
else:
raise NotImplementedError
# After creating condition node, we link it declaration of the loop variable
link_underlying_nodes(counter_node, node_condition)
# Create an expression for the loop increment (counter_var += 1)
loop_increment = AugAssign(
"-1:-1:-1",
-1,
target=Name("-1:-1:-1", -1, "counter_var"),
op="+=",
value=Int("-1:-1:-1", -1, 1),
)
node_increment = self._new_node(NodeType.EXPRESSION, expr.src, scope)
node_increment.add_unparsed_expression(loop_increment)
link_underlying_nodes(node_increment, node_condition)
continue_destination = node_increment
break_destination = node_endLoop
# We assign the index variable or range variable in the loop body on each iteration
expr.body.insert(0, loop_var)
body_node = None
new_node = node_condition
for stmt in expr.body:
body_node = parse_statement(
new_node, stmt, continue_destination, break_destination
)
new_node = body_node
if body_node is not None:
link_underlying_nodes(body_node, node_increment)
link_underlying_nodes(node_condition, node_endLoop)
curr_node = node_endLoop
elif isinstance(expr, Continue):
new_node = self._new_node(NodeType.CONTINUE, expr.src, scope)
link_underlying_nodes(curr_node, new_node)
link_underlying_nodes(new_node, continue_destination)
elif isinstance(expr, Break):
new_node = self._new_node(NodeType.BREAK, expr.src, scope)
link_underlying_nodes(curr_node, new_node)
link_underlying_nodes(new_node, break_destination)
elif isinstance(expr, Return):
new_node = self._new_node(NodeType.RETURN, expr.src, scope)
if expr.value is not None:
new_node.add_unparsed_expression(expr.value)
link_underlying_nodes(curr_node, new_node)
curr_node = new_node
elif isinstance(expr, Assert):
new_node = self._new_node(NodeType.EXPRESSION, expr.src, scope)
new_node.add_unparsed_expression(expr)
link_underlying_nodes(curr_node, new_node)
curr_node = new_node
elif isinstance(expr, Log):
new_node = self._new_node(NodeType.EXPRESSION, expr.src, scope)
new_node.add_unparsed_expression(expr.value)
link_underlying_nodes(curr_node, new_node)
curr_node = new_node
elif isinstance(expr, If):
condition_node = self._new_node(NodeType.IF, expr.test.src, scope)
condition_node.add_unparsed_expression(expr.test)
endIf_node = self._new_node(NodeType.ENDIF, expr.src, scope)
true_node = None
new_node = condition_node
for stmt in expr.body:
true_node = parse_statement(
new_node, stmt, continue_destination, break_destination
)
new_node = true_node
link_underlying_nodes(true_node, endIf_node)
false_node = None
new_node = condition_node
for stmt in expr.orelse:
false_node = parse_statement(
new_node, stmt, continue_destination, break_destination
)
new_node = false_node
if false_node is not None:
link_underlying_nodes(false_node, endIf_node)
else:
link_underlying_nodes(condition_node, endIf_node)
link_underlying_nodes(curr_node, condition_node)
curr_node = endIf_node
elif isinstance(expr, Pass):
pass
elif isinstance(expr, Raise):
new_node = self._new_node(NodeType.EXPRESSION, expr.src, scope)
new_node.add_unparsed_expression(expr)
link_underlying_nodes(curr_node, new_node)
curr_node = new_node
else:
raise ParsingError(f"Statement not parsed {expr.__class__.__name__} {expr}")
return curr_node
curr_node = entry_node
for expr in cfg:
curr_node = parse_statement(curr_node, expr)
# endregion
###################################################################################
###################################################################################
def _add_param(self, param: Arg, initialized: bool = False) -> LocalVariableVyper:
local_var = LocalVariable()
local_var.set_function(self._function)
local_var.set_offset(param.src, self._function.compilation_unit)
local_var_parser = LocalVariableVyper(local_var, param)
if initialized:
local_var.initialized = True
if local_var.location == "default":
local_var.set_location("memory")
self._add_local_variable(local_var_parser)
return local_var_parser
def _parse_params(self, params: Arguments):
self._function.parameters_src().set_offset(params.src, self._function.compilation_unit)
if params.defaults:
self._function._default_args_as_expressions = params.defaults
for param in params.args:
local_var = self._add_param(param)
self._function.add_parameters(local_var.underlying_variable)
def _parse_returns(self, returns: Union[Name, TupleVyper, Subscript]):
self._function.returns_src().set_offset(returns.src, self._function.compilation_unit)
# Only the type of the arg is given, not a name. We create an an `Arg` with an empty name
# so that the function has the correct return type in its signature but doesn't clash with
# other identifiers during name resolution (`find_variable`).
if isinstance(returns, (Name, Subscript)):
local_var = self._add_param(Arg(returns.src, returns.node_id, "", annotation=returns))
self._function.add_return(local_var.underlying_variable)
else:
assert isinstance(returns, TupleVyper)
for ret in returns.elements:
local_var = self._add_param(Arg(ret.src, ret.node_id, "", annotation=ret))
self._function.add_return(local_var.underlying_variable)
###################################################################################
###################################################################################

@ -0,0 +1,33 @@
from typing import List
from slither.core.declarations.structure import Structure
from slither.core.variables.structure_variable import StructureVariable
from slither.vyper_parsing.variables.structure_variable import StructureVariableVyper
from slither.vyper_parsing.ast.types import StructDef, AnnAssign
class StructVyper: # pylint: disable=too-few-public-methods
def __init__(
self,
st: Structure,
struct: StructDef,
) -> None:
self._structure = st
st.name = struct.name
st.canonical_name = struct.name + self._structure.contract.name
self._elemsNotParsed: List[AnnAssign] = struct.body
def analyze(self, contract) -> None:
for elem_to_parse in self._elemsNotParsed:
elem = StructureVariable()
elem.set_structure(self._structure)
elem.set_offset(elem_to_parse.src, self._structure.contract.compilation_unit)
elem_parser = StructureVariableVyper(elem, elem_to_parse)
elem_parser.analyze(contract)
self._structure.elems[elem.name] = elem
self._structure.add_elem_in_order(elem.name)
self._elemsNotParsed = []

@ -0,0 +1,464 @@
from typing import Optional, List, Union, TYPE_CHECKING
from collections import deque
from slither.core.declarations.solidity_variables import (
SOLIDITY_VARIABLES_COMPOSED,
SolidityVariableComposed,
)
from slither.core.declarations import SolidityFunction, FunctionContract
from slither.core.variables.state_variable import StateVariable
from slither.core.expressions import (
CallExpression,
ElementaryTypeNameExpression,
Identifier,
IndexAccess,
Literal,
MemberAccess,
SelfIdentifier,
TupleExpression,
TypeConversion,
UnaryOperation,
UnaryOperationType,
)
from slither.core.expressions.assignment_operation import (
AssignmentOperation,
AssignmentOperationType,
)
from slither.core.expressions.binary_operation import (
BinaryOperation,
BinaryOperationType,
)
from slither.core.solidity_types import (
ArrayType,
ElementaryType,
UserDefinedType,
)
from slither.core.declarations.contract import Contract
from slither.vyper_parsing.expressions.find_variable import find_variable
from slither.vyper_parsing.type_parsing import parse_type
from slither.all_exceptions import ParsingError
from slither.vyper_parsing.ast.types import (
Int,
Call,
Attribute,
Name,
Tuple,
Hex,
BinOp,
Str,
Assert,
Compare,
UnaryOp,
Subscript,
NameConstant,
VyDict,
Bytes,
BoolOp,
Assign,
AugAssign,
VyList,
Raise,
ASTNode,
)
if TYPE_CHECKING:
from slither.core.expressions.expression import Expression
def vars_to_typestr(rets: Optional[List["Expression"]]) -> str:
if rets is None:
return "tuple()"
if len(rets) == 1:
return str(rets[0].type)
return f"tuple({','.join(str(ret.type) for ret in rets)})"
# pylint: disable=too-many-branches,too-many-statements,too-many-locals
def parse_expression(
expression: ASTNode, caller_context: Union[FunctionContract, Contract]
) -> "Expression":
if isinstance(expression, Int):
literal = Literal(str(expression.value), ElementaryType("uint256"))
literal.set_offset(expression.src, caller_context.compilation_unit)
return literal
if isinstance(expression, Hex):
# TODO this is an implicit conversion and could potentially be bytes20 or other? https://github.com/vyperlang/vyper/issues/3580
literal = Literal(str(expression.value), ElementaryType("address"))
literal.set_offset(expression.src, caller_context.compilation_unit)
return literal
if isinstance(expression, Str):
literal = Literal(str(expression.value), ElementaryType("string"))
literal.set_offset(expression.src, caller_context.compilation_unit)
return literal
if isinstance(expression, Bytes):
literal = Literal(str(expression.value), ElementaryType("bytes"))
literal.set_offset(expression.src, caller_context.compilation_unit)
return literal
if isinstance(expression, NameConstant):
assert str(expression.value) in ["True", "False"]
literal = Literal(str(expression.value), ElementaryType("bool"))
literal.set_offset(expression.src, caller_context.compilation_unit)
return literal
if isinstance(expression, Call):
called = parse_expression(expression.func, caller_context)
if isinstance(called, Identifier) and isinstance(called.value, SolidityFunction):
if called.value.name == "empty()":
type_to = parse_type(expression.args[0], caller_context)
# TODO figure out how to represent this type argument
parsed_expr = CallExpression(called, [], str(type_to))
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
if called.value.name == "convert()":
arg = parse_expression(expression.args[0], caller_context)
type_to = parse_type(expression.args[1], caller_context)
parsed_expr = TypeConversion(arg, type_to)
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
if called.value.name == "min_value()":
type_to = parse_type(expression.args[0], caller_context)
member_type = str(type_to)
# TODO return Literal
parsed_expr = MemberAccess(
"min",
member_type,
CallExpression(
Identifier(SolidityFunction("type()")),
[ElementaryTypeNameExpression(type_to)],
member_type,
),
)
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
if called.value.name == "max_value()":
type_to = parse_type(expression.args[0], caller_context)
member_type = str(type_to)
# TODO return Literal
parsed_expr = MemberAccess(
"max",
member_type,
CallExpression(
Identifier(SolidityFunction("type()")),
[ElementaryTypeNameExpression(type_to)],
member_type,
),
)
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
if called.value.name == "raw_call()":
args = [parse_expression(a, caller_context) for a in expression.args]
# This is treated specially in order to force `extract_tmp_call` to treat this as a `HighLevelCall` which will be converted
# to a `LowLevelCall` by `convert_to_low_level`. This is an artifact of the late conversion of Solidity...
call = CallExpression(
MemberAccess("raw_call", "tuple(bool,bytes32)", args[0]),
args[1:],
"tuple(bool,bytes32)",
)
call.set_offset(expression.src, caller_context.compilation_unit)
call.call_value = next(
iter(
parse_expression(x.value, caller_context)
for x in expression.keywords
if x.arg == "value"
),
None,
)
call.call_gas = next(
iter(
parse_expression(x.value, caller_context)
for x in expression.keywords
if x.arg == "gas"
),
None,
)
# TODO handle `max_outsize` keyword
return call
if expression.args and isinstance(expression.args[0], VyDict):
arguments = []
for val in expression.args[0].values:
arguments.append(parse_expression(val, caller_context))
else:
arguments = [parse_expression(a, caller_context) for a in expression.args]
rets = None
# Since the AST lacks the type of the return values, we recover it. https://github.com/vyperlang/vyper/issues/3581
if isinstance(called, Identifier):
if isinstance(called.value, FunctionContract):
rets = called.value.returns
# Default arguments are not represented in the AST, so we recover them as well.
# pylint: disable=protected-access
if called.value._default_args_as_expressions and len(arguments) < len(
called.value.parameters
):
arguments.extend(
[
parse_expression(x, caller_context)
for x in called.value._default_args_as_expressions
]
)
elif isinstance(called.value, SolidityFunction):
rets = called.value.return_type
elif isinstance(called.value, Contract):
# Type conversions are not explicitly represented in the AST e.g. converting address to contract/ interface,
# so we infer that a type conversion is occurring if `called` is a `Contract` type. https://github.com/vyperlang/vyper/issues/3580
type_to = parse_type(expression.func, caller_context)
parsed_expr = TypeConversion(arguments[0], type_to)
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
elif isinstance(called, MemberAccess) and called.type is not None:
# (recover_type_2) Propagate the type collected to the `CallExpression`
# see recover_type_1
rets = [called]
type_str = vars_to_typestr(rets)
parsed_expr = CallExpression(called, arguments, type_str)
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
if isinstance(expression, Attribute):
member_name = expression.attr
if isinstance(expression.value, Name):
# TODO this is ambiguous because it could be a state variable or a call to balance https://github.com/vyperlang/vyper/issues/3582
if expression.value.id == "self" and member_name != "balance":
var = find_variable(member_name, caller_context, is_self=True)
parsed_expr = SelfIdentifier(var)
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
var.references.append(parsed_expr.source_mapping)
return parsed_expr
expr = parse_expression(expression.value, caller_context)
# TODO this is ambiguous because it could be a type conversion of an interface or a member access
# see https://github.com/vyperlang/vyper/issues/3580 and ttps://github.com/vyperlang/vyper/issues/3582
if expression.attr == "address":
parsed_expr = TypeConversion(expr, ElementaryType("address"))
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
member_access = MemberAccess(member_name, None, expr)
if str(member_access) in SOLIDITY_VARIABLES_COMPOSED:
parsed_expr = Identifier(SolidityVariableComposed(str(member_access)))
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
else:
expr = parse_expression(expression.value, caller_context)
member_name_ret_type = None
# (recover_type_1) This may be a call to an interface and we don't have the return types,
# so we see if there's a function identifier with `member_name` and propagate the type to
# its enclosing `CallExpression`. https://github.com/vyperlang/vyper/issues/3581
if (
isinstance(expr, Identifier)
and isinstance(expr.value, StateVariable)
and isinstance(expr.value.type, UserDefinedType)
and isinstance(expr.value.type.type, Contract)
):
# If we access a member of an interface, needs to be interface instead of self namespace
var = find_variable(member_name, expr.value.type.type)
if isinstance(var, FunctionContract):
rets = var.returns
member_name_ret_type = vars_to_typestr(rets)
if (
isinstance(expr, TypeConversion)
and isinstance(expr.type, UserDefinedType)
and isinstance(expr.type.type, Contract)
):
# If we access a member of an interface, needs to be interface instead of self namespace
var = find_variable(member_name, expr.type.type)
if isinstance(var, FunctionContract):
rets = var.returns
member_name_ret_type = vars_to_typestr(rets)
member_access = MemberAccess(member_name, member_name_ret_type, expr)
member_access.set_offset(expression.src, caller_context.compilation_unit)
return member_access
if isinstance(expression, Name):
var = find_variable(expression.id, caller_context)
parsed_expr = Identifier(var)
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
if isinstance(expression, Assign):
lhs = parse_expression(expression.target, caller_context)
rhs = parse_expression(expression.value, caller_context)
parsed_expr = AssignmentOperation(lhs, rhs, AssignmentOperationType.ASSIGN, None)
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
if isinstance(expression, AugAssign):
lhs = parse_expression(expression.target, caller_context)
rhs = parse_expression(expression.value, caller_context)
op = AssignmentOperationType.get_type(expression.op)
parsed_expr = AssignmentOperation(lhs, rhs, op, None)
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
if isinstance(expression, (Tuple, VyList)):
tuple_vars = [parse_expression(x, caller_context) for x in expression.elements]
parsed_expr = TupleExpression(tuple_vars)
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
if isinstance(expression, UnaryOp):
operand = parse_expression(expression.operand, caller_context)
op = UnaryOperationType.get_type(
expression.op, isprefix=True
) # TODO does vyper have postfix?
parsed_expr = UnaryOperation(operand, op)
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
if isinstance(expression, Compare):
lhs = parse_expression(expression.left, caller_context)
# We assume left operand in membership comparison cannot be Array type
if expression.op in ["In", "NotIn"]:
# If we see a membership operator e.g. x in [foo(), bar()], we convert it to logical operations
# like (x == foo() || x == bar()) or (x != foo() && x != bar()) for "not in"
# TODO consider rewriting as if-else to accurately represent the precedence of potential side-effects
conditions = deque()
rhs = parse_expression(expression.right, caller_context)
is_tuple = isinstance(rhs, TupleExpression)
is_array = isinstance(rhs, Identifier) and isinstance(rhs.value.type, ArrayType)
if is_array:
assert (
rhs.value.type.is_fixed_array
), "Dynamic arrays are not supported in comparison operators"
if is_tuple or is_array:
length = len(rhs.expressions) if is_tuple else rhs.value.type.length_value.value
inner_op = (
BinaryOperationType.get_type("!=")
if expression.op == "NotIn"
else BinaryOperationType.get_type("==")
)
for i in range(length):
elem_expr = (
rhs.expressions[i]
if is_tuple
else IndexAccess(rhs, Literal(str(i), ElementaryType("uint256")))
)
elem_expr.set_offset(rhs.source_mapping, caller_context.compilation_unit)
parsed_expr = BinaryOperation(lhs, elem_expr, inner_op)
parsed_expr.set_offset(lhs.source_mapping, caller_context.compilation_unit)
conditions.append(parsed_expr)
outer_op = (
BinaryOperationType.get_type("&&")
if expression.op == "NotIn"
else BinaryOperationType.get_type("||")
)
while len(conditions) > 1:
lhs = conditions.pop()
rhs = conditions.pop()
conditions.appendleft(BinaryOperation(lhs, rhs, outer_op))
return conditions.pop()
# enum type membership check https://docs.vyperlang.org/en/stable/types.html?h#id18
is_member_op = (
BinaryOperationType.get_type("==")
if expression.op == "NotIn"
else BinaryOperationType.get_type("!=")
)
# If all bits are cleared, then the lhs is not a member of the enum
# This allows representing membership in multiple enum members
# For example, if enum Foo has members A (1), B (2), and C (4), then
# (x in [Foo.A, Foo.B]) is equivalent to (x & (Foo.A | Foo.B) != 0),
# where (Foo.A | Foo.B) evaluates to 3.
# Thus, when x is 3, (x & (Foo.A | Foo.B) != 0) is true.
enum_bit_mask = BinaryOperation(
TypeConversion(lhs, ElementaryType("uint256")),
TypeConversion(rhs, ElementaryType("uint256")),
BinaryOperationType.get_type("&"),
)
membership_check = BinaryOperation(
enum_bit_mask, Literal("0", ElementaryType("uint256")), is_member_op
)
membership_check.set_offset(lhs.source_mapping, caller_context.compilation_unit)
return membership_check
# a regular logical operator
rhs = parse_expression(expression.right, caller_context)
op = BinaryOperationType.get_type(expression.op)
parsed_expr = BinaryOperation(lhs, rhs, op)
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
if isinstance(expression, BinOp):
lhs = parse_expression(expression.left, caller_context)
rhs = parse_expression(expression.right, caller_context)
op = BinaryOperationType.get_type(expression.op)
parsed_expr = BinaryOperation(lhs, rhs, op)
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
if isinstance(expression, Assert):
# Treat assert the same as a Solidity `require`.
# TODO rename from `SolidityFunction` to `Builtin`?
type_str = "tuple()"
if expression.msg is None:
func = SolidityFunction("require(bool)")
args = [parse_expression(expression.test, caller_context)]
else:
func = SolidityFunction("require(bool,string)")
args = [
parse_expression(expression.test, caller_context),
parse_expression(expression.msg, caller_context),
]
parsed_expr = CallExpression(Identifier(func), args, type_str)
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
if isinstance(expression, Subscript):
left_expression = parse_expression(expression.value, caller_context)
right_expression = parse_expression(expression.slice.value, caller_context)
parsed_expr = IndexAccess(left_expression, right_expression)
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
if isinstance(expression, BoolOp):
lhs = parse_expression(expression.values[0], caller_context)
rhs = parse_expression(expression.values[1], caller_context)
op = BinaryOperationType.get_type(expression.op)
parsed_expr = BinaryOperation(lhs, rhs, op)
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
if isinstance(expression, Raise):
type_str = "tuple()"
func = (
SolidityFunction("revert()")
if expression.exc is None
else SolidityFunction("revert(string)")
)
args = [] if expression.exc is None else [parse_expression(expression.exc, caller_context)]
parsed_expr = CallExpression(Identifier(func), args, type_str)
parsed_expr.set_offset(expression.src, caller_context.compilation_unit)
return parsed_expr
raise ParsingError(f"Expression not parsed {expression}")

@ -0,0 +1,150 @@
from typing import TYPE_CHECKING, Optional, Union, Tuple
from slither.core.declarations import Event, Enum, Structure
from slither.core.declarations.contract import Contract
from slither.core.declarations.custom_error import CustomError
from slither.core.declarations.function import Function
from slither.core.declarations.function_contract import FunctionContract
from slither.core.declarations.solidity_variables import (
SOLIDITY_FUNCTIONS,
SOLIDITY_VARIABLES,
SolidityFunction,
SolidityVariable,
)
from slither.core.variables.variable import Variable
from slither.solc_parsing.exceptions import VariableNotFound
if TYPE_CHECKING:
from slither.vyper_parsing.declarations.function import FunctionVyper
def _find_variable_in_function_parser(
var_name: str,
function_parser: Optional["FunctionVyper"],
) -> Optional[Variable]:
if function_parser is None:
return None
func_variables = function_parser.variables_as_dict
if var_name in func_variables:
return func_variables[var_name]
return None
def _find_in_contract(
var_name: str,
contract: Optional[Contract],
contract_declarer: Optional[Contract],
) -> Optional[Union[Variable, Function, Contract, Event, Enum, Structure, CustomError]]:
if contract is None or contract_declarer is None:
return None
# variable are looked from the contract declarer
contract_variables = contract.variables_as_dict
if var_name in contract_variables:
return contract_variables[var_name]
functions = {f.name: f for f in contract.functions if not f.is_shadowed}
if var_name in functions:
return functions[var_name]
# structures are looked on the contract declarer
structures = contract.structures_as_dict
if var_name in structures:
return structures[var_name]
events = contract.events_as_dict
if var_name in events:
return events[var_name]
enums = contract.enums_as_dict
if var_name in enums:
return enums[var_name]
# If the enum is referred as its name rather than its canonicalName
enums = {e.name: e for e in contract.enums}
if var_name in enums:
return enums[var_name]
return None
def find_variable(
var_name: str,
caller_context: Union[FunctionContract, Contract],
is_self: bool = False,
) -> Tuple[
Union[
Variable,
Function,
Contract,
SolidityVariable,
SolidityFunction,
Event,
Enum,
Structure,
]
]:
"""
Return the variable found and a boolean indicating if the variable was created
If the variable was created, it has no source mapping, and it the caller must add it
:param var_name:
:type var_name:
:param caller_context:
:type caller_context:
:param is_self:
:type is_self:
:return:
:rtype:
"""
# pylint: disable=import-outside-toplevel
from slither.vyper_parsing.declarations.function import (
FunctionVyper,
)
if isinstance(caller_context, Contract):
current_scope = caller_context.file_scope
next_context = caller_context
else:
current_scope = caller_context.contract.file_scope
next_context = caller_context.contract
function_parser: Optional[FunctionVyper] = (
caller_context if isinstance(caller_context, FunctionContract) else None
)
# If a local shadows a state variable but the attribute is `self`, we want to
# return the state variable and not the local.
if not is_self:
ret1 = _find_variable_in_function_parser(var_name, function_parser)
if ret1:
return ret1
ret = _find_in_contract(var_name, next_context, caller_context)
if ret:
return ret
if var_name in current_scope.variables:
return current_scope.variables[var_name]
# Could refer to any enum
all_enumss = [c.enums_as_dict for c in current_scope.contracts.values()]
all_enums = {k: v for d in all_enumss for k, v in d.items()}
if var_name in all_enums:
return all_enums[var_name]
contracts = current_scope.contracts
if var_name in contracts:
return contracts[var_name]
if var_name in SOLIDITY_VARIABLES:
return SolidityVariable(var_name)
if f"{var_name}()" in SOLIDITY_FUNCTIONS:
return SolidityFunction(f"{var_name}()")
if f"{var_name}()" in next_context.events_as_dict:
return next_context.events_as_dict[f"{var_name}()"]
raise VariableNotFound(f"Variable not found: {var_name} (context {caller_context})")

@ -0,0 +1,99 @@
from typing import Union
from slither.core.solidity_types.elementary_type import (
ElementaryType,
ElementaryTypeName,
) # TODO rename solidity type
from slither.core.solidity_types.array_type import ArrayType
from slither.core.solidity_types.mapping_type import MappingType
from slither.core.solidity_types.user_defined_type import UserDefinedType
from slither.core.declarations import FunctionContract, Contract
from slither.vyper_parsing.ast.types import Name, Subscript, Call, Index, Tuple
from slither.solc_parsing.exceptions import ParsingError
# pylint: disable=too-many-branches,too-many-return-statements,import-outside-toplevel,too-many-locals
def parse_type(
annotation: Union[Name, Subscript, Call, Tuple],
caller_context: Union[FunctionContract, Contract],
):
from slither.vyper_parsing.expressions.expression_parsing import parse_expression
if isinstance(caller_context, FunctionContract):
contract = caller_context.contract
else:
contract = caller_context
assert isinstance(annotation, (Name, Subscript, Call, Tuple))
if isinstance(annotation, Name):
name = annotation.id
lname = name.lower() # map `String` to string
if lname in ElementaryTypeName:
return ElementaryType(lname)
if name in contract.structures_as_dict:
return UserDefinedType(contract.structures_as_dict[name])
if name in contract.enums_as_dict:
return UserDefinedType(contract.enums_as_dict[name])
if name in contract.file_scope.contracts:
return UserDefinedType(contract.file_scope.contracts[name])
if name in contract.file_scope.structures:
return UserDefinedType(contract.file_scope.structures[name])
elif isinstance(annotation, Subscript):
assert isinstance(annotation.slice, Index)
# This is also a strange construct... https://github.com/vyperlang/vyper/issues/3577
if isinstance(annotation.slice.value, Tuple):
assert isinstance(annotation.value, Name)
if annotation.value.id == "DynArray":
type_ = parse_type(annotation.slice.value.elements[0], caller_context)
length = parse_expression(annotation.slice.value.elements[1], caller_context)
return ArrayType(type_, length)
if annotation.value.id == "HashMap":
type_from = parse_type(annotation.slice.value.elements[0], caller_context)
type_to = parse_type(annotation.slice.value.elements[1], caller_context)
return MappingType(type_from, type_to)
elif isinstance(annotation.value, Subscript):
type_ = parse_type(annotation.value, caller_context)
elif isinstance(annotation.value, Name):
# TODO it is weird that the ast_type is `Index` when it's a type annotation and not an expression, so we grab the value. https://github.com/vyperlang/vyper/issues/3577
type_ = parse_type(annotation.value, caller_context)
if annotation.value.id == "String":
# This is an elementary type
return type_
length = parse_expression(annotation.slice.value, caller_context)
return ArrayType(type_, length)
elif isinstance(annotation, Call):
# TODO event variable represented as Call https://github.com/vyperlang/vyper/issues/3579
return parse_type(annotation.args[0], caller_context)
elif isinstance(annotation, Tuple):
# Vyper has tuple types like python x = f() where f() -> (y,z)
# and tuple elements can be unpacked like x[0]: y and x[1]: z.
# We model these as a struct and unpack each index into a field
# e.g. accessing the 0th element is translated as x._0
from slither.core.declarations.structure import Structure
from slither.core.variables.structure_variable import StructureVariable
st = Structure(caller_context.compilation_unit)
st.set_offset("-1:-1:-1", caller_context.compilation_unit)
st.name = "FAKE_TUPLE"
for idx, elem_info in enumerate(annotation.elements):
elem = StructureVariable()
elem.type = parse_type(elem_info, caller_context)
elem.name = f"_{idx}"
elem.set_structure(st)
elem.set_offset("-1:-1:-1", caller_context.compilation_unit)
st.elems[elem.name] = elem
st.add_elem_in_order(elem.name)
st.name += elem.name
return UserDefinedType(st)
raise ParsingError(f"Type name not found {name} context {caller_context}")

@ -0,0 +1,24 @@
from slither.core.variables.event_variable import EventVariable
from slither.vyper_parsing.type_parsing import parse_type
from slither.vyper_parsing.ast.types import AnnAssign, Call
class EventVariableVyper:
def __init__(self, variable: EventVariable, variable_data: AnnAssign):
self._variable = variable
self._variable.name = variable_data.target.id
if (
isinstance(variable_data.annotation, Call)
and variable_data.annotation.func.id == "indexed"
):
self._variable.indexed = True
else:
self._variable.indexed = False
self._elem_to_parse = variable_data.annotation
@property
def underlying_variable(self) -> EventVariable:
return self._variable
def analyze(self, contract) -> None:
self._variable.type = parse_type(self._elem_to_parse, contract)

@ -0,0 +1,34 @@
from typing import Union
from slither.core.variables.local_variable import LocalVariable
from slither.vyper_parsing.ast.types import Arg, Name, AnnAssign, Subscript, Call, Tuple
from slither.vyper_parsing.type_parsing import parse_type
class LocalVariableVyper:
def __init__(self, variable: LocalVariable, variable_data: Union[Arg, AnnAssign, Name]) -> None:
self._variable: LocalVariable = variable
if isinstance(variable_data, Arg):
self._variable.name = variable_data.arg
self._elem_to_parse = variable_data.annotation
elif isinstance(variable_data, AnnAssign):
self._variable.name = variable_data.target.id
self._elem_to_parse = variable_data.annotation
else:
assert isinstance(variable_data, Name)
self._variable.name = variable_data.id
self._elem_to_parse = variable_data
assert isinstance(self._elem_to_parse, (Name, Subscript, Call, Tuple))
# Vyper does not have data locations or storage pointers.
# If this was left as default, reference types would be considered storage by `LocalVariable.is_storage`
self._variable.set_location("memory")
@property
def underlying_variable(self) -> LocalVariable:
return self._variable
def analyze(self, contract) -> None:
self._variable.type = parse_type(self._elem_to_parse, contract)

@ -0,0 +1,29 @@
from slither.core.variables.state_variable import StateVariable
from slither.vyper_parsing.ast.types import VariableDecl
from slither.vyper_parsing.type_parsing import parse_type
from slither.vyper_parsing.expressions.expression_parsing import parse_expression
class StateVariableVyper:
def __init__(self, variable: StateVariable, variable_data: VariableDecl) -> None:
self._variable: StateVariable = variable
self._variable.name = variable_data.target.id
self._variable.is_constant = variable_data.is_constant
self._variable.is_immutable = variable_data.is_immutable
self._variable.visibility = "public" if variable_data.is_public else "internal"
self._elem_to_parse = variable_data.annotation
if variable_data.value is not None:
self._variable.initialized = True
self._initializedNotParsed = variable_data.value
@property
def underlying_variable(self) -> StateVariable:
return self._variable
def analyze(self, contract) -> None:
self._variable.type = parse_type(self._elem_to_parse, contract)
if self._variable.initialized:
self._variable.expression = parse_expression(self._initializedNotParsed, contract)
self._initializedNotParsed = None

@ -0,0 +1,17 @@
from slither.core.variables.structure_variable import StructureVariable
from slither.vyper_parsing.type_parsing import parse_type
from slither.vyper_parsing.ast.types import AnnAssign
class StructureVariableVyper:
def __init__(self, variable: StructureVariable, variable_data: AnnAssign):
self._variable: StructureVariable = variable
self._variable.name = variable_data.target.id
self._elem_to_parse = variable_data.annotation
@property
def underlying_variable(self) -> StructureVariable:
return self._variable
def analyze(self, contract) -> None:
self._variable.type = parse_type(self._elem_to_parse, contract)

@ -0,0 +1,80 @@
from typing import Dict
import os
import re
from dataclasses import dataclass, field
from slither.core.declarations import Contract
from slither.core.compilation_unit import SlitherCompilationUnit
from slither.vyper_parsing.declarations.contract import ContractVyper
from slither.analyses.data_dependency.data_dependency import compute_dependency
from slither.vyper_parsing.ast.types import Module
from slither.exceptions import SlitherException
@dataclass
class VyperCompilationUnit:
_compilation_unit: SlitherCompilationUnit
_parsed: bool = False
_analyzed: bool = False
_underlying_contract_to_parser: Dict[Contract, ContractVyper] = field(default_factory=dict)
_contracts_by_id: Dict[int, Contract] = field(default_factory=dict)
def parse_module(self, data: Module, filename: str):
sourceUnit_candidates = re.findall("[0-9]*:[0-9]*:([0-9]*)", data.src)
assert len(sourceUnit_candidates) == 1, "Source unit not found"
sourceUnit = int(sourceUnit_candidates[0])
self._compilation_unit.source_units[sourceUnit] = filename
if os.path.isfile(filename) and filename not in self._compilation_unit.core.source_code:
self._compilation_unit.core.add_source_code(filename)
scope = self._compilation_unit.get_scope(filename)
contract = Contract(self._compilation_unit, scope)
contract_parser = ContractVyper(self, contract, data)
contract.set_offset(data.src, self._compilation_unit)
self._underlying_contract_to_parser[contract] = contract_parser
def parse_contracts(self):
for contract, contract_parser in self._underlying_contract_to_parser.items():
self._contracts_by_id[contract.id] = contract
self._compilation_unit.contracts.append(contract)
contract_parser.parse_enums()
contract_parser.parse_structs()
contract_parser.parse_state_variables()
contract_parser.parse_events()
contract_parser.parse_functions()
self._parsed = True
def analyze_contracts(self) -> None:
if not self._parsed:
raise SlitherException("Parse the contract before running analyses")
for contract_parser in self._underlying_contract_to_parser.values():
# State variables are analyzed for all contracts because interfaces may
# reference them, specifically, constants.
contract_parser.analyze_state_variables()
for contract_parser in self._underlying_contract_to_parser.values():
contract_parser.analyze()
self._convert_to_slithir()
compute_dependency(self._compilation_unit)
self._analyzed = True
def _convert_to_slithir(self) -> None:
for contract in self._compilation_unit.contracts:
contract.add_constructor_variables()
for func in contract.functions:
func.generate_slithir_and_analyze()
contract.convert_expression_to_slithir_ssa()
self._compilation_unit.propagate_function_calls()
for contract in self._compilation_unit.contracts:
contract.fix_phi()
contract.update_read_write_using_ssa()

@ -59,7 +59,7 @@ def solc_binary_path(shared_directory):
@pytest.fixture
def slither_from_source(solc_binary_path):
def slither_from_solidity_source(solc_binary_path):
@contextmanager
def inner(source_code: str, solc_version: str = "0.8.19"):
"""Yields a Slither instance using source_code string and solc_version.
@ -77,3 +77,23 @@ def slither_from_source(solc_binary_path):
Path(fname).unlink()
return inner
@pytest.fixture
def slither_from_vyper_source():
@contextmanager
def inner(source_code: str):
"""Yields a Slither instance using source_code string.
Creates a temporary file and compiles with vyper.
"""
fname = ""
try:
with tempfile.NamedTemporaryFile(mode="w", suffix=".vy", delete=False) as f:
fname = f.name
f.write(source_code)
yield Slither(fname)
finally:
Path(fname).unlink()
return inner

@ -0,0 +1,5 @@
contract TestSlither {
function testFunction(uint256 param1, uint256, address param3) public {
}
}

@ -43,3 +43,20 @@ def test_cycle(solc_binary_path) -> None:
solc_path = solc_binary_path("0.8.0")
slither = Slither(Path(TEST_DATA_DIR, "test_cyclic_import", "a.sol").as_posix(), solc=solc_path)
_run_all_detectors(slither)
def test_contract_function_parameter(solc_binary_path) -> None:
solc_path = solc_binary_path("0.8.0")
standard_json = SolcStandardJson()
standard_json.add_source_file(
Path(TEST_DATA_DIR, "test_contract_data", "test_contract_data.sol").as_posix()
)
compilation = CryticCompile(standard_json, solc=solc_path)
slither = Slither(compilation)
contract = slither.contracts[0]
function = contract.functions[0]
parameters = function.parameters
assert (parameters[0].name == 'param1')
assert (parameters[1].name == '')
assert (parameters[2].name == 'param3')

@ -1,20 +1,20 @@
Loop condition `j < array.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#109) should use cached array length instead of referencing `length` member of the storage array.
Loop condition k_scope_17 < array2.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#133) should use cached array length instead of referencing `length` member of the storage array.
Loop condition `i < array.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#161) should use cached array length instead of referencing `length` member of the storage array.
Loop condition i_scope_23 < array.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#172) should use cached array length instead of referencing `length` member of the storage array.
Loop condition `i < array.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#172) should use cached array length instead of referencing `length` member of the storage array.
Loop condition i < array.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#37) should use cached array length instead of referencing `length` member of the storage array.
Loop condition `j < array.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#126) should use cached array length instead of referencing `length` member of the storage array.
Loop condition j_scope_11 < array.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#109) should use cached array length instead of referencing `length` member of the storage array.
Loop condition `k < array2.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#133) should use cached array length instead of referencing `length` member of the storage array.
Loop condition i_scope_4 < array.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#68) should use cached array length instead of referencing `length` member of the storage array.
Loop condition `i < array.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#68) should use cached array length instead of referencing `length` member of the storage array.
Loop condition i_scope_22 < array.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#167) should use cached array length instead of referencing `length` member of the storage array.
Loop condition `k < array2.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#99) should use cached array length instead of referencing `length` member of the storage array.
Loop condition k_scope_9 < array2.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#99) should use cached array length instead of referencing `length` member of the storage array.
Loop condition `i < array.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#167) should use cached array length instead of referencing `length` member of the storage array.
Loop condition i_scope_6 < array.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#80) should use cached array length instead of referencing `length` member of the storage array.
Loop condition `i < array.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#37) should use cached array length instead of referencing `length` member of the storage array.
Loop condition j_scope_15 < array.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#126) should use cached array length instead of referencing `length` member of the storage array.
Loop condition `i < array.length` (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#80) should use cached array length instead of referencing `length` member of the storage array.
Loop condition i_scope_21 < array.length (tests/e2e/detectors/test_data/cache-array-length/0.8.17/CacheArrayLength.sol#161) should use cached array length instead of referencing `length` member of the storage array.

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save