Merge branch 'dev' into dev-isdynamic

pull/1175/head
Josselin Feist 2 years ago
commit 879157cfc1
  1. 12
      .github/workflows/IR.yml
  2. 7
      .github/workflows/black.yml
  3. 27
      .github/workflows/ci.yml
  4. 12
      .github/workflows/detectors.yml
  5. 13
      .github/workflows/features.yml
  6. 7
      .github/workflows/linter.yml
  7. 12
      .github/workflows/parser.yml
  8. 23
      .github/workflows/pip-audit.yml
  9. 7
      .github/workflows/pylint.yml
  10. 50
      .github/workflows/read_storage.yml
  11. 24
      CONTRIBUTING.md
  12. 11
      Dockerfile
  13. 35
      README.md
  14. 3
      examples/scripts/taint_mapping.py
  15. 2
      plugin_example/setup.py
  16. 2
      scripts/ci_test_kspec.sh
  17. 4
      scripts/ci_test_simil.sh
  18. 2
      scripts/ci_test_truffle.sh
  19. 18
      setup.py
  20. 43
      slither/__main__.py
  21. 4
      slither/core/cfg/node.py
  22. 2
      slither/core/children/child_node.py
  23. 2
      slither/core/compilation_unit.py
  24. 32
      slither/core/declarations/contract.py
  25. 11
      slither/core/declarations/enum.py
  26. 2
      slither/core/declarations/function.py
  27. 2
      slither/core/declarations/solidity_variables.py
  28. 9
      slither/core/slither_core.py
  29. 10
      slither/core/solidity_types/array_type.py
  30. 8
      slither/core/solidity_types/elementary_type.py
  31. 3
      slither/core/solidity_types/type_information.py
  32. 6
      slither/detectors/all_detectors.py
  33. 4
      slither/detectors/assembly/shift_parameter_mixup.py
  34. 2
      slither/detectors/attributes/incorrect_solc.py
  35. 0
      slither/detectors/erc/erc20/__init__.py
  36. 95
      slither/detectors/erc/erc20/arbitrary_send_erc20.py
  37. 45
      slither/detectors/erc/erc20/arbitrary_send_erc20_no_permit.py
  38. 53
      slither/detectors/erc/erc20/arbitrary_send_erc20_permit.py
  39. 0
      slither/detectors/erc/erc20/incorrect_erc20_interface.py
  40. 6
      slither/detectors/functions/arbitrary_send_eth.py
  41. 2
      slither/detectors/naming_convention/naming_convention.py
  42. 8
      slither/detectors/statements/calls_in_loop.py
  43. 8
      slither/detectors/statements/costly_operations_in_loop.py
  44. 8
      slither/detectors/statements/delegatecall_in_loop.py
  45. 8
      slither/detectors/statements/msg_value_in_loop.py
  46. 12
      slither/detectors/statements/too_many_digits.py
  47. 2
      slither/detectors/statements/unprotected_upgradeable.py
  48. 4
      slither/detectors/variables/similar_variables.py
  49. 6
      slither/detectors/variables/uninitialized_local_variables.py
  50. 2
      slither/detectors/variables/uninitialized_storage_variables.py
  51. 2
      slither/formatters/attributes/constant_pragma.py
  52. 3
      slither/formatters/attributes/incorrect_solc.py
  53. 6
      slither/formatters/naming_convention/naming_convention.py
  54. 2
      slither/printers/summary/variable_order.py
  55. 2
      slither/slither.py
  56. 42
      slither/slithir/convert.py
  57. 5
      slither/solc_parsing/declarations/contract.py
  58. 3
      slither/solc_parsing/expressions/expression_parsing.py
  59. 21
      slither/solc_parsing/slither_compilation_unit_solc.py
  60. 12
      slither/solc_parsing/solidity_types/type_parsing.py
  61. 4
      slither/solc_parsing/yul/parse_yul.py
  62. 1
      slither/tools/erc_conformance/erc/ercs.py
  63. 7
      slither/tools/flattening/__main__.py
  64. 38
      slither/tools/flattening/flattening.py
  65. 4
      slither/tools/kspec_coverage/analysis.py
  66. 91
      slither/tools/read_storage/README.md
  67. 1
      slither/tools/read_storage/__init__.py
  68. 145
      slither/tools/read_storage/__main__.py
  69. 551
      slither/tools/read_storage/read_storage.py
  70. 11
      slither/tools/read_storage/utils/__init__.py
  71. 100
      slither/tools/read_storage/utils/utils.py
  72. 6
      slither/tools/upgradeability/checks/initialization.py
  73. 8
      slither/utils/colors.py
  74. 49
      slither/utils/erc.py
  75. 77
      slither/utils/expression_manipulations.py
  76. 2
      slither/utils/integer_conversion.py
  77. 2
      slither/utils/output.py
  78. 16
      slither/visitors/slithir/expression_to_slithir.py
  79. BIN
      tests/ast-parsing/compile/assembly-all.sol-0.8.13-compact.zip
  80. BIN
      tests/ast-parsing/compile/assembly-all.sol-0.8.14-compact.zip
  81. BIN
      tests/ast-parsing/compile/assembly-all.sol-0.8.15-compact.zip
  82. BIN
      tests/ast-parsing/compile/assignment-0.4.7.sol-0.8.13-compact.zip
  83. BIN
      tests/ast-parsing/compile/assignment-0.4.7.sol-0.8.14-compact.zip
  84. BIN
      tests/ast-parsing/compile/assignment-0.4.7.sol-0.8.15-compact.zip
  85. BIN
      tests/ast-parsing/compile/break-all.sol-0.8.13-compact.zip
  86. BIN
      tests/ast-parsing/compile/break-all.sol-0.8.14-compact.zip
  87. BIN
      tests/ast-parsing/compile/break-all.sol-0.8.15-compact.zip
  88. BIN
      tests/ast-parsing/compile/call_to_variable-all.sol-0.8.13-compact.zip
  89. BIN
      tests/ast-parsing/compile/call_to_variable-all.sol-0.8.14-compact.zip
  90. BIN
      tests/ast-parsing/compile/call_to_variable-all.sol-0.8.15-compact.zip
  91. BIN
      tests/ast-parsing/compile/comment-all.sol-0.8.13-compact.zip
  92. BIN
      tests/ast-parsing/compile/comment-all.sol-0.8.14-compact.zip
  93. BIN
      tests/ast-parsing/compile/comment-all.sol-0.8.15-compact.zip
  94. BIN
      tests/ast-parsing/compile/complex_imports/import_aliases/test.sol-0.8.13-compact.zip
  95. BIN
      tests/ast-parsing/compile/complex_imports/import_aliases/test.sol-0.8.14-compact.zip
  96. BIN
      tests/ast-parsing/compile/complex_imports/import_aliases/test.sol-0.8.15-compact.zip
  97. BIN
      tests/ast-parsing/compile/conditional-all.sol-0.8.13-compact.zip
  98. BIN
      tests/ast-parsing/compile/conditional-all.sol-0.8.14-compact.zip
  99. BIN
      tests/ast-parsing/compile/conditional-all.sol-0.8.15-compact.zip
  100. BIN
      tests/ast-parsing/compile/continue-all.sol-0.8.13-compact.zip
  101. Some files were not shown because too many files have changed in this diff Show More

@ -26,20 +26,14 @@ jobs:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python 3.6 - name: Set up Python 3.8
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
python-version: 3.6 python-version: 3.8
- name: Install dependencies - name: Install dependencies
run: | run: |
python setup.py install pip install ".[dev]"
pip install deepdiff
pip install pytest==7.0.1
pip install typing_extensions==4.1.1
pip install importlib_metadata==4.8.3
pip install "solc-select>=v1.0.0b1"
solc-select install all solc-select install all
solc-select use 0.8.11 solc-select use 0.8.11

@ -22,16 +22,13 @@ jobs:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python 3.6 - name: Set up Python 3.8
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
python-version: 3.6 python-version: 3.8
- name: Install dependencies - name: Install dependencies
run: | run: |
pip install .
pip install deepdiff numpy
mkdir -p .github/linters mkdir -p .github/linters
cp pyproject.toml .github/linters cp pyproject.toml .github/linters

@ -3,8 +3,7 @@ name: CI
defaults: defaults:
run: run:
# To load bashrc shell: bash
shell: bash -ieo pipefail {0}
on: on:
push: push:
@ -28,7 +27,7 @@ jobs:
"data_dependency", "data_dependency",
# "embark", # "embark",
"erc", "erc",
"etherlime", # "etherlime",
# "etherscan" # "etherscan"
"find_paths", "find_paths",
"flat", "flat",
@ -44,32 +43,17 @@ jobs:
- os: windows-2022 - os: windows-2022
type: dapp type: dapp
# Requires nvm # Requires nvm
- os: windows-2022
type: etherlime
# Requires nvm
- os: windows-2022 - os: windows-2022
type: truffle type: truffle
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Set up Python 3.8
- name: Set up shell
if: runner.os == 'Windows'
run: |
echo 'C:\msys64\mingw64\bin' >> "$GITHUB_PATH"
echo 'C:\msys64\usr\bin' >> "$GITHUB_PATH"
- name: Set up Python 3.6
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
python-version: 3.6 python-version: 3.8
- name: Install dependencies - name: Install dependencies
run: | run: |
pip install ".[dev]"
python setup.py install
# Used by ci_test.sh
pip install deepdiff
pip install "solc-select>=v1.0.0b1"
solc-select install all solc-select install all
solc-select use 0.5.1 solc-select use 0.5.1
pip install typing_extensions==4.1.1 pip install typing_extensions==4.1.1
@ -87,6 +71,7 @@ jobs:
- name: Run Tests - name: Run Tests
env: env:
PYTHONUTF8: 1
TEST_TYPE: ${{ matrix.type }} TEST_TYPE: ${{ matrix.type }}
GITHUB_ETHERSCAN: ${{ secrets.GITHUB_ETHERSCAN }} GITHUB_ETHERSCAN: ${{ secrets.GITHUB_ETHERSCAN }}
run: | run: |

@ -26,21 +26,15 @@ jobs:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python 3.6 - name: Set up Python 3.8
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
python-version: 3.6 python-version: 3.8
- name: Install dependencies - name: Install dependencies
run: | run: |
pip install ".[dev]"
python setup.py install
pip install deepdiff
pip install pytest==7.0.1
pip install typing_extensions==4.1.1
pip install importlib_metadata==4.8.3
pip install "solc-select>=v1.0.0b1"
solc-select install all solc-select install all
solc-select use 0.7.3 solc-select use 0.7.3
- name: Test with pytest - name: Test with pytest

@ -26,21 +26,15 @@ jobs:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python 3.6 - name: Set up Python 3.8
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
python-version: 3.6 python-version: 3.8
- name: Install dependencies - name: Install dependencies
run: | run: |
pip install ".[dev]"
python setup.py install
pip install deepdiff
pip install pytest==7.0.1
pip install typing_extensions==4.1.1
pip install importlib_metadata==4.8.3
pip install "solc-select>=v1.0.0b1"
solc-select install all solc-select install all
solc-select use 0.8.0 solc-select use 0.8.0
@ -52,3 +46,4 @@ jobs:
run: | run: |
pytest tests/test_features.py pytest tests/test_features.py
pytest tests/test_constant_folding_unary.py pytest tests/test_constant_folding_unary.py
pytest tests/slithir/test_ternary_expressions.py

@ -22,16 +22,13 @@ jobs:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python 3.6 - name: Set up Python 3.8
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
python-version: 3.6 python-version: 3.8
- name: Install dependencies - name: Install dependencies
run: | run: |
pip install .
pip install deepdiff numpy
mkdir -p .github/linters mkdir -p .github/linters
cp pyproject.toml .github/linters cp pyproject.toml .github/linters

@ -26,20 +26,14 @@ jobs:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python 3.6 - name: Set up Python 3.8
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
python-version: 3.6 python-version: 3.8
- name: Install dependencies - name: Install dependencies
run: | run: |
pip install ".[dev]"
python setup.py install
pip install deepdiff
pip install pytest==7.0.1
pip install typing_extensions==4.1.1
pip install importlib_metadata==4.8.3
pip install "solc-select>=v1.0.0b1"
- name: Install solc - name: Install solc
run: | run: |

@ -11,18 +11,25 @@ on:
jobs: jobs:
audit: audit:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Set up Python 3.10
uses: actions/setup-python@v3 - name: Install Python
uses: actions/setup-python@v4
with: with:
python-version: "3.10" python-version: "3.10"
- name: Install pip-audit
- name: Install Slither
run: | run: |
python -m venv /tmp/pip-audit-env
source /tmp/pip-audit-env/bin/activate
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install pip-audit
- name: Run pip-audit
run: |
python -m pip install . python -m pip install .
pip-audit --desc -v
- name: Run pip-audit
uses: trailofbits/gh-action-pip-audit@v0.0.4
with:
virtual-environment: /tmp/pip-audit-env

@ -22,16 +22,13 @@ jobs:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python 3.6 - name: Set up Python 3.8
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
python-version: 3.6 python-version: 3.8
- name: Install dependencies - name: Install dependencies
run: | run: |
pip install .
pip install deepdiff numpy
mkdir -p .github/linters mkdir -p .github/linters
cp pyproject.toml .github/linters cp pyproject.toml .github/linters

@ -0,0 +1,50 @@
---
name: Test slither-read-storage
defaults:
run:
# To load bashrc
shell: bash -ieo pipefail {0}
on:
pull_request:
branches: [master, dev]
schedule:
# run CI every day even if no PRs/merges occur
- cron: '0 12 * * *'
jobs:
build:
name: Test slither-read-storage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: '14'
- name: Install ganache
run: npm install --global ganache
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install python dependencies
run: |
pip install ".[dev]"
pip install web3
solc-select install 0.8.1
solc-select install 0.8.10
solc-select use 0.8.1
- name: Run slither-read-storage
run: |
pytest tests/test_read_storage.py
- name: Run storage layout tests
run: |
pytest tests/test_storage_layout.py

@ -20,13 +20,28 @@ Some pull request guidelines:
- Fill out the pull request description with a summary of what your patch does, key changes that have been made, and any further points of discussion, if applicable. - Fill out the pull request description with a summary of what your patch does, key changes that have been made, and any further points of discussion, if applicable.
- Title your pull request with a brief description of what it's changing. "Fixes #123" is a good comment to add to the description, but makes for an unclear title on its own. - Title your pull request with a brief description of what it's changing. "Fixes #123" is a good comment to add to the description, but makes for an unclear title on its own.
## Directory Structure
Below is a rough outline of slither's design:
```text
.
├── analyses # Provides additional info such as data dependency
├── core # Ties everything together
├── detectors # Rules that define and identify issues
├── slither.py # Main entry point
├── slithir # Contains the semantics of slither's intermediate representation
├── solc_parsing # Responsible for parsing the solc AST
├── tools # Miscellaneous tools built on top of slither
├── visitors # Parses expressions and converts to slithir
└── ...
```
A code walkthrough is available [here](https://www.youtube.com/watch?v=EUl3UlYSluU).
## Development Environment ## Development Environment
Instructions for installing a development version of Slither can be found in our [wiki](https://github.com/crytic/slither/wiki/Developer-installation). Instructions for installing a development version of Slither can be found in our [wiki](https://github.com/crytic/slither/wiki/Developer-installation).
To run the unit tests, you need To run the unit tests, you need to clone this repository and run `pip install ".[dev]"`.
- `deepdiff` installed (`pip install deepdiff`).
- `pycov` installed (`pip install pytest-cov`).
- [`solc-select`](https://github.com/crytic/solc-select) installed.
### Linters ### Linters
@ -46,6 +61,7 @@ For each new detector, at least one regression tests must be present.
- Create a test in `tests` - Create a test in `tests`
- Update `ALL_TEST` in `tests/test_detectors.py` - Update `ALL_TEST` in `tests/test_detectors.py`
- Run `python ./tests/test_detectors.py --generate`. This will generate the json artifacts in `tests/expected_json`. Add the generated files to git. - Run `python ./tests/test_detectors.py --generate`. This will generate the json artifacts in `tests/expected_json`. Add the generated files to git.
- If updating an existing detector, identify the respective json artifacts and then delete them, or run `python ./tests/test_detectors.py --overwrite` instead.
- Run `pytest ./tests/test_detectors.py` and check that everything worked. - Run `pytest ./tests/test_detectors.py` and check that everything worked.
To see the tests coverage, run `pytest tests/test_detectors.py --cov=slither/detectors --cov-branch --cov-report html` To see the tests coverage, run `pytest tests/test_detectors.py --cov=slither/detectors --cov-branch --cov-report html`

@ -1,4 +1,4 @@
FROM ubuntu:bionic FROM ubuntu:focal
LABEL name=slither LABEL name=slither
LABEL src="https://github.com/trailofbits/slither" LABEL src="https://github.com/trailofbits/slither"
@ -6,11 +6,12 @@ LABEL creator=trailofbits
LABEL dockerfile_maintenance=trailofbits LABEL dockerfile_maintenance=trailofbits
LABEL desc="Static Analyzer for Solidity" LABEL desc="Static Analyzer for Solidity"
RUN apt-get update \ RUN export DEBIAN_FRONTEND=noninteractive \
&& apt-get upgrade -y \ && apt-get update \
&& apt-get install -y git python3 python3-setuptools wget software-properties-common && apt-get upgrade -yq \
&& apt-get install -yq gcc git python3 python3-dev python3-setuptools wget software-properties-common
RUN wget https://github.com/ethereum/solidity/releases/download/v0.4.25/solc-static-linux \ RUN wget -q https://github.com/ethereum/solidity/releases/download/v0.4.25/solc-static-linux \
&& chmod +x solc-static-linux \ && chmod +x solc-static-linux \
&& mv solc-static-linux /usr/bin/solc && mv solc-static-linux /usr/bin/solc

@ -13,6 +13,7 @@ Slither is a Solidity static analysis framework written in Python 3. It runs a s
- [Tools](#tools) - [Tools](#tools)
- [How to Install](#how-to-install) - [How to Install](#how-to-install)
- [Getting Help](#getting-help) - [Getting Help](#getting-help)
- [FAQ](#faq)
- [Publications](#publications) - [Publications](#publications)
## Features ## Features
@ -40,9 +41,12 @@ Run Slither on a single file:
slither tests/uninitialized.sol slither tests/uninitialized.sol
``` ```
For GitHub action integration, see [slither-action](https://github.com/marketplace/actions/slither-action). For additional configuration, see the [usage](https://github.com/trailofbits/slither/wiki/Usage) documentation. ### 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`)
Use [solc-select](https://github.com/crytic/solc-select) if your contracts require older versions of solc. Use [solc-select](https://github.com/crytic/solc-select) if your contracts require older versions of solc. For additional configuration, see the [usage](https://github.com/trailofbits/slither/wiki/Usage) documentation.
### Detectors ### Detectors
@ -51,7 +55,7 @@ Num | Detector | What it Detects | Impact | Confidence
--- | --- | --- | --- | --- --- | --- | --- | --- | ---
1 | `abiencoderv2-array` | [Storage abiencoderv2 array](https://github.com/crytic/slither/wiki/Detector-Documentation#storage-abiencoderv2-array) | High | High 1 | `abiencoderv2-array` | [Storage abiencoderv2 array](https://github.com/crytic/slither/wiki/Detector-Documentation#storage-abiencoderv2-array) | High | High
2 | `array-by-reference` | [Modifying storage array by value](https://github.com/crytic/slither/wiki/Detector-Documentation#modifying-storage-array-by-value) | High | High 2 | `array-by-reference` | [Modifying storage array by value](https://github.com/crytic/slither/wiki/Detector-Documentation#modifying-storage-array-by-value) | High | High
3 | `incorrect-shift` | [The order of parameters in a shift instruction is incorrect.](https://github.com/crytic/slither/wiki/Detector-Documentation#shift-parameter-mixup) | High | High 3 | `incorrect-shift` | [The order of parameters in a shift instruction is incorrect.](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-shift-in-assembly) | High | High
4 | `multiple-constructors` | [Multiple constructor schemes](https://github.com/crytic/slither/wiki/Detector-Documentation#multiple-constructor-schemes) | High | High 4 | `multiple-constructors` | [Multiple constructor schemes](https://github.com/crytic/slither/wiki/Detector-Documentation#multiple-constructor-schemes) | High | High
5 | `name-reused` | [Contract's name reused](https://github.com/crytic/slither/wiki/Detector-Documentation#name-reused) | High | High 5 | `name-reused` | [Contract's name reused](https://github.com/crytic/slither/wiki/Detector-Documentation#name-reused) | High | High
6 | `public-mappings-nested` | [Public mappings with nested variables](https://github.com/crytic/slither/wiki/Detector-Documentation#public-mappings-with-nested-variables) | High | High 6 | `public-mappings-nested` | [Public mappings with nested variables](https://github.com/crytic/slither/wiki/Detector-Documentation#public-mappings-with-nested-variables) | High | High
@ -121,10 +125,12 @@ Num | Detector | What it Detects | Impact | Confidence
70 | `costly-loop` | [Costly operations in a loop](https://github.com/crytic/slither/wiki/Detector-Documentation#costly-operations-inside-a-loop) | Informational | Medium 70 | `costly-loop` | [Costly operations in a loop](https://github.com/crytic/slither/wiki/Detector-Documentation#costly-operations-inside-a-loop) | Informational | Medium
71 | `dead-code` | [Functions that are not used](https://github.com/crytic/slither/wiki/Detector-Documentation#dead-code) | Informational | Medium 71 | `dead-code` | [Functions that are not used](https://github.com/crytic/slither/wiki/Detector-Documentation#dead-code) | Informational | Medium
72 | `reentrancy-unlimited-gas` | [Reentrancy vulnerabilities through send and transfer](https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-4) | Informational | Medium 72 | `reentrancy-unlimited-gas` | [Reentrancy vulnerabilities through send and transfer](https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-4) | Informational | Medium
73 | `similar-names` | [Variable names are too similar](https://github.com/crytic/slither/wiki/Detector-Documentation#variable-names-are-too-similar) | Informational | Medium 73 | `similar-names` | [Variable names are too similar](https://github.com/crytic/slither/wiki/Detector-Documentation#variable-names-too-similar) | Informational | Medium
74 | `too-many-digits` | [Conformance to numeric notation best practices](https://github.com/crytic/slither/wiki/Detector-Documentation#too-many-digits) | Informational | Medium 74 | `too-many-digits` | [Conformance to numeric notation best practices](https://github.com/crytic/slither/wiki/Detector-Documentation#too-many-digits) | Informational | Medium
75 | `constable-states` | [State variables that could be declared constant](https://github.com/crytic/slither/wiki/Detector-Documentation#state-variables-that-could-be-declared-constant) | Optimization | High 75 | `constable-states` | [State variables that could be declared constant](https://github.com/crytic/slither/wiki/Detector-Documentation#state-variables-that-could-be-declared-constant) | Optimization | High
76 | `external-function` | [Public function that could be declared external](https://github.com/crytic/slither/wiki/Detector-Documentation#public-function-that-could-be-declared-external) | Optimization | High 76 | `external-function` | [Public function that could be declared external](https://github.com/crytic/slither/wiki/Detector-Documentation#public-function-that-could-be-declared-external) | Optimization | High
77 | `arbitrary-send-erc20` | [Detect when `msg.sender` is not used as `from` in transferFrom](https://github.com/trailofbits/slither/wiki/Detector-Documentation#arbitrary-send-erc20)
78 | `arbitrary-send-erc20-permit` | [Detect when `msg.sender` is not used as `from` in transferFrom in conjuction with permit](https://github.com/trailofbits/slither/wiki/Detector-Documentation#arbitrary-send-erc20-permit)
For more information, see For more information, see
- The [Detector Documentation](https://github.com/crytic/slither/wiki/Detector-Documentation) for details on each detector - The [Detector Documentation](https://github.com/crytic/slither/wiki/Detector-Documentation) for details on each detector
@ -143,6 +149,7 @@ For more information, see
- `cfg`: [Export the CFG of each functions](https://github.com/trailofbits/slither/wiki/Printer-documentation#cfg) - `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) - `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) - `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)
- `when-not-paused`: [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. To run a printer, use `--print` and a comma-separated list of printers.
@ -155,6 +162,7 @@ See the [Printer documentation](https://github.com/crytic/slither/wiki/Printer-d
- `slither-flat`: [Flatten a codebase](https://github.com/crytic/slither/wiki/Contract-Flattening) - `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-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-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)
See the [Tool documentation](https://github.com/crytic/slither/wiki/Tool-Documentation) for additional tools. See the [Tool documentation](https://github.com/crytic/slither/wiki/Tool-Documentation) for additional tools.
@ -162,7 +170,7 @@ See the [Tool documentation](https://github.com/crytic/slither/wiki/Tool-Documen
## How to install ## How to install
Slither requires Python 3.6+ and [solc](https://github.com/ethereum/solidity/), the Solidity compiler. Slither requires Python 3.8+ and [solc](https://github.com/ethereum/solidity/), the Solidity compiler.
### Using Pip ### Using Pip
@ -205,11 +213,23 @@ Feel free to stop by our [Slack channel](https://empireslacking.herokuapp.com) (
* 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).
How do I fix "unknown file" or compilation issues?
- 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,
run the compilation command for the framework you are using e.g `npx hardhat compile`. That must work successfully;
otherwise, slither's compilation engine, crytic-compile, cannot generate the AST.
## License ## License
Slither is licensed and distributed under the AGPLv3 license. [Contact us](mailto:opensource@trailofbits.com) if you're looking for an exception to the terms. Slither is licensed and distributed under the AGPLv3 license. [Contact us](mailto:opensource@trailofbits.com) if you're looking for an exception to the terms.
## Publications ## Publications
### Trail of Bits publication ### Trail of Bits publication
@ -225,5 +245,8 @@ Title | Usage | Authors | Venue
[Smart Contract Repair](https://arxiv.org/pdf/1912.05823.pdf) | Rely on Slither’s vulnerabilities detectors | Xiao Liang Yu, Omar Al-Bataineh, David Lo, Abhik Roychoudhury | TOSEM 20 [Smart Contract Repair](https://arxiv.org/pdf/1912.05823.pdf) | Rely on Slither’s vulnerabilities detectors | Xiao Liang Yu, Omar Al-Bataineh, David Lo, Abhik Roychoudhury | TOSEM 20
[Demystifying Loops in Smart Contracts](https://www.microsoft.com/en-us/research/uploads/prod/2020/08/loops_solidity__camera_ready-5f3fec3f15c69.pdf) | Leverage data dependency through Slither | Ben Mariano, Yanju Chen, Yu Feng, Shuvendu Lahiri, Isil Dillig | ASE 20 [Demystifying Loops in Smart Contracts](https://www.microsoft.com/en-us/research/uploads/prod/2020/08/loops_solidity__camera_ready-5f3fec3f15c69.pdf) | Leverage data dependency through Slither | Ben Mariano, Yanju Chen, Yu Feng, Shuvendu Lahiri, Isil Dillig | ASE 20
[Trace-Based Dynamic Gas Estimation of Loops in Smart Contracts](https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=9268144) | Use Slither’s CFG to detect loops | Chunmiao Li, Shijie Nie, Yang Cao, Yijun Yu, Zhenjiang Hu | IEEE Open J. Comput. Soc. 1 (2020) [Trace-Based Dynamic Gas Estimation of Loops in Smart Contracts](https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=9268144) | Use Slither’s CFG to detect loops | Chunmiao Li, Shijie Nie, Yang Cao, Yijun Yu, Zhenjiang Hu | IEEE Open J. Comput. Soc. 1 (2020)
[SAILFISH: Vetting Smart Contract State-Inconsistency Bugs in Seconds](https://arxiv.org/pdf/2104.08638.pdf) | Rely on SlithIR to build a *storage dependency graph* | Priyanka Bose, Dipanjan Das, Yanju Chen, Yu Feng, Christopher Kruegel, and Giovanni Vigna | S&P 22
[SolType: Refinement Types for Arithmetic Overflow in Solidity](https://arxiv.org/abs/2110.00677) | Use Slither as frontend to build refinement type system | Bryan Tan, Benjamin Mariano, Shuvendu K. Lahiri, Isil Dillig, Yu Feng | POPL 22
[Do Not Rug on Me: Leveraging Machine Learning Techniques for Automated Scam Detection](https://www.mdpi.com/2227-7390/10/6/949) | Use Slither to extract tokens' features (mintable, pausable, ..) | Mazorra, Bruno, Victor Adan, and Vanesa Daza | Mathematics 10.6 (2022)
If you are using Slither on an academic work, consider applying to the [Crytic $10k Research Prize](https://blog.trailofbits.com/2019/11/13/announcing-the-crytic-10k-research-prize/). If you are using Slither on an academic work, consider applying to the [Crytic $10k Research Prize](https://blog.trailofbits.com/2019/11/13/announcing-the-crytic-10k-research-prize/).

@ -10,6 +10,9 @@ from slither.slithir.variables.temporary import TemporaryVariable
def visit_node(node, visited): def visit_node(node, visited):
if node is None:
return
if node in visited: if node in visited:
return return

@ -7,7 +7,7 @@ setup(
author="Trail of Bits", author="Trail of Bits",
version="0.0", version="0.0",
packages=find_packages(), packages=find_packages(),
python_requires=">=3.6", python_requires=">=3.8",
install_requires=["slither-analyzer==0.1"], install_requires=["slither-analyzer==0.1"],
entry_points={ entry_points={
"slither_analyzer.plugin": "slither my-plugin=slither_my_plugin:make_plugin", "slither_analyzer.plugin": "slither my-plugin=slither_my_plugin:make_plugin",

@ -7,7 +7,7 @@ slither-check-kspec "$DIR_TESTS/safeAdd/safeAdd.sol" "$DIR_TESTS/safeAdd/spec.md
DIFF=$(diff test_1.txt "$DIR_TESTS/test_1.txt") DIFF=$(diff test_1.txt "$DIR_TESTS/test_1.txt")
if [ "$DIFF" != "" ] if [ "$DIFF" != "" ]
then then
echo "slither-check-upgradeability 1 failed" echo "slither-check-kspec 1 failed"
cat test_1.txt cat test_1.txt
echo "" echo ""
cat "$DIR_TESTS/test_1.txt" cat "$DIR_TESTS/test_1.txt"

@ -2,8 +2,8 @@
### Install requisites ### Install requisites
pip3.6 install pybind11 pip3.8 install pybind11
pip3.6 install https://github.com/facebookresearch/fastText/archive/0.2.0.zip pip3.8 install https://github.com/facebookresearch/fastText/archive/0.2.0.zip
### Test slither-simil ### Test slither-simil

@ -15,7 +15,7 @@ npm install -g truffle
truffle unbox metacoin truffle unbox metacoin
slither . slither .
if [ $? -eq 9 ] if [ $? -eq 6 ]
then then
exit 0 exit 0
fi fi

@ -8,15 +8,26 @@ setup(
description="Slither is a Solidity static analysis framework written in Python 3.", description="Slither is a Solidity static analysis framework written in Python 3.",
url="https://github.com/crytic/slither", url="https://github.com/crytic/slither",
author="Trail of Bits", author="Trail of Bits",
version="0.8.2", version="0.8.3",
packages=find_packages(), packages=find_packages(),
python_requires=">=3.6", python_requires=">=3.8",
install_requires=[ install_requires=[
"prettytable>=0.7.2", "prettytable>=0.7.2",
"pysha3>=1.0.2", "pysha3>=1.0.2",
# "crytic-compile>=0.2.2", # "crytic-compile>=0.2.3",
"crytic-compile", "crytic-compile",
], ],
extras_require={
"dev": [
"black==22.3.0",
"pylint==2.13.4",
"pytest",
"pytest-cov",
"deepdiff",
"numpy",
"solc-select>=v1.0.0b1",
]
},
dependency_links=["git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile"], dependency_links=["git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile"],
license="AGPL-3.0", license="AGPL-3.0",
long_description=long_description, long_description=long_description,
@ -32,6 +43,7 @@ setup(
"slither-check-kspec = slither.tools.kspec_coverage.__main__:main", "slither-check-kspec = slither.tools.kspec_coverage.__main__:main",
"slither-prop = slither.tools.properties.__main__:main", "slither-prop = slither.tools.properties.__main__:main",
"slither-mutate = slither.tools.mutator.__main__:main", "slither-mutate = slither.tools.mutator.__main__:main",
"slither-read-storage = slither.tools.read_storage.__main__:main",
] ]
}, },
) )

@ -299,6 +299,9 @@ def parse_args(detector_classes, printer_classes): # pylint: disable=too-many-s
group_detector = parser.add_argument_group("Detectors") group_detector = parser.add_argument_group("Detectors")
group_printer = parser.add_argument_group("Printers") group_printer = parser.add_argument_group("Printers")
group_checklist = parser.add_argument_group(
"Checklist (consider using https://github.com/crytic/slither-action)"
)
group_misc = parser.add_argument_group("Additional options") group_misc = parser.add_argument_group("Additional options")
group_detector.add_argument( group_detector.add_argument(
@ -312,7 +315,7 @@ def parse_args(detector_classes, printer_classes): # pylint: disable=too-many-s
group_printer.add_argument( group_printer.add_argument(
"--print", "--print",
help="Comma-separated list fo contract information printers, " help="Comma-separated list of contract information printers, "
f"available printers: {', '.join(d.ARGUMENT for d in printer_classes)}", f"available printers: {', '.join(d.ARGUMENT for d in printer_classes)}",
action="store", action="store",
dest="printers_to_run", dest="printers_to_run",
@ -392,6 +395,28 @@ def parse_args(detector_classes, printer_classes): # pylint: disable=too-many-s
default=defaults_flag_in_config["show_ignored_findings"], default=defaults_flag_in_config["show_ignored_findings"],
) )
group_checklist.add_argument(
"--checklist",
help="Generate a markdown page with the detector results",
action="store_true",
default=False,
)
group_checklist.add_argument(
"--checklist-limit",
help="Limite the number of results per detector in the markdown file",
action="store",
default="",
)
group_checklist.add_argument(
"--markdown-root",
type=check_and_sanitize_markdown_root,
help="URL for markdown generation",
action="store",
default="",
)
group_misc.add_argument( group_misc.add_argument(
"--json", "--json",
help='Export the results as a JSON file ("--json -" to export to stdout)', help='Export the results as a JSON file ("--json -" to export to stdout)',
@ -429,14 +454,6 @@ def parse_args(detector_classes, printer_classes): # pylint: disable=too-many-s
default=defaults_flag_in_config["zip_type"], default=defaults_flag_in_config["zip_type"],
) )
group_misc.add_argument(
"--markdown-root",
type=check_and_sanitize_markdown_root,
help="URL for markdown generation",
action="store",
default="",
)
group_misc.add_argument( group_misc.add_argument(
"--disable-color", "--disable-color",
help="Disable output colorization", help="Disable output colorization",
@ -487,12 +504,6 @@ def parse_args(detector_classes, printer_classes): # pylint: disable=too-many-s
parser.add_argument("--markdown", help=argparse.SUPPRESS, action=OutputMarkdown, default=False) parser.add_argument("--markdown", help=argparse.SUPPRESS, action=OutputMarkdown, default=False)
group_misc.add_argument(
"--checklist", help=argparse.SUPPRESS, action="store_true", default=False
)
group_misc.add_argument("--checklist-limit", help=argparse.SUPPRESS, action="store", default="")
parser.add_argument( parser.add_argument(
"--wiki-detectors", help=argparse.SUPPRESS, action=OutputWiki, default=False "--wiki-detectors", help=argparse.SUPPRESS, action=OutputWiki, default=False
) )
@ -648,7 +659,7 @@ def main_impl(all_detector_classes, all_printer_classes):
cp.enable() cp.enable()
# Set colorization option # Set colorization option
set_colorization_enabled(not args.disable_color) set_colorization_enabled(False if args.disable_color else sys.stdout.isatty())
# Define some variables for potential JSON output # Define some variables for potential JSON output
json_results = {} json_results = {}

@ -39,12 +39,11 @@ from slither.slithir.variables import (
TupleVariable, TupleVariable,
) )
from slither.all_exceptions import SlitherException from slither.all_exceptions import SlitherException
from slither.core.declarations import Contract from slither.core.declarations import Contract, Function
from slither.core.expressions.expression import Expression from slither.core.expressions.expression import Expression
if TYPE_CHECKING: if TYPE_CHECKING:
from slither.core.declarations import Function
from slither.slithir.variables.variable import SlithIRVariable from slither.slithir.variables.variable import SlithIRVariable
from slither.core.compilation_unit import SlitherCompilationUnit from slither.core.compilation_unit import SlitherCompilationUnit
from slither.utils.type_helpers import ( from slither.utils.type_helpers import (
@ -917,6 +916,7 @@ class Node(SourceMapping, ChildFunction): # pylint: disable=too-many-public-met
) )
elif isinstance(ir, LibraryCall): elif isinstance(ir, LibraryCall):
assert isinstance(ir.destination, Contract) assert isinstance(ir.destination, Contract)
assert isinstance(ir.function, Function)
self._high_level_calls.append((ir.destination, ir.function)) self._high_level_calls.append((ir.destination, ir.function))
self._library_calls.append((ir.destination, ir.function)) self._library_calls.append((ir.destination, ir.function))

@ -28,4 +28,4 @@ class ChildNode:
@property @property
def compilation_unit(self) -> "SlitherCompilationUnit": def compilation_unit(self) -> "SlitherCompilationUnit":
return self.contract.compilation_unit return self.node.compilation_unit

@ -251,7 +251,7 @@ class SlitherCompilationUnit(Context):
slot = 0 slot = 0
offset = 0 offset = 0
for var in contract.state_variables_ordered: for var in contract.state_variables_ordered:
if var.is_constant: if var.is_constant or var.is_immutable:
continue continue
size, new_slot = var.type.storage_size size, new_slot = var.type.storage_size

@ -21,6 +21,8 @@ from slither.utils.erc import (
ERC777_signatures, ERC777_signatures,
ERC1155_signatures, ERC1155_signatures,
ERC2612_signatures, ERC2612_signatures,
ERC1363_signatures,
ERC4524_signatures,
ERC4626_signatures, ERC4626_signatures,
) )
from slither.utils.tests_pattern import is_test_contract from slither.utils.tests_pattern import is_test_contract
@ -78,6 +80,7 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
self._using_for: Dict[Union[str, Type], List[Type]] = {} self._using_for: Dict[Union[str, Type], List[Type]] = {}
self._kind: Optional[str] = None self._kind: Optional[str] = None
self._is_interface: bool = False self._is_interface: bool = False
self._is_library: bool = False
self._signatures: Optional[List[str]] = None self._signatures: Optional[List[str]] = None
self._signatures_declared: Optional[List[str]] = None self._signatures_declared: Optional[List[str]] = None
@ -144,6 +147,14 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
def is_interface(self, is_interface: bool): def is_interface(self, is_interface: bool):
self._is_interface = is_interface self._is_interface = is_interface
@property
def is_library(self) -> bool:
return self._is_library
@is_library.setter
def is_library(self, is_library: bool):
self._is_library = is_library
# endregion # endregion
################################################################################### ###################################################################################
################################################################################### ###################################################################################
@ -903,6 +914,7 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
("ERC721", self.is_erc721), ("ERC721", self.is_erc721),
("ERC777", self.is_erc777), ("ERC777", self.is_erc777),
("ERC2612", self.is_erc2612), ("ERC2612", self.is_erc2612),
("ERC1363", self.is_erc1363),
("ERC4626", self.is_erc4626), ("ERC4626", self.is_erc4626),
] ]
@ -998,6 +1010,26 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
full_names = self.functions_signatures full_names = self.functions_signatures
return all(s in full_names for s in ERC2612_signatures) return all(s in full_names for s in ERC2612_signatures)
def is_erc1363(self) -> bool:
"""
Check if the contract is an erc1363
Note: it does not check for correct return values
:return: Returns a true if the contract is an erc1363
"""
full_names = self.functions_signatures
return all(s in full_names for s in ERC1363_signatures)
def is_erc4524(self) -> bool:
"""
Check if the contract is an erc4524
Note: it does not check for correct return values
:return: Returns a true if the contract is an erc4524
"""
full_names = self.functions_signatures
return all(s in full_names for s in ERC4524_signatures)
@property @property
def is_token(self) -> bool: def is_token(self) -> bool:
""" """

@ -9,6 +9,9 @@ class Enum(SourceMapping):
self._name = name self._name = name
self._canonical_name = canonical_name self._canonical_name = canonical_name
self._values = values self._values = values
self._min = 0
# The max value of an Enum is the index of the last element
self._max = len(values) - 1
@property @property
def canonical_name(self) -> str: def canonical_name(self) -> str:
@ -22,5 +25,13 @@ class Enum(SourceMapping):
def values(self) -> List[str]: def values(self) -> List[str]:
return self._values return self._values
@property
def min(self) -> int:
return self._min
@property
def max(self) -> int:
return self._max
def __str__(self): def __str__(self):
return self.name return self.name

@ -538,7 +538,7 @@ class Function(SourceMapping, metaclass=ABCMeta): # pylint: disable=too-many-pu
self._nodes = nodes self._nodes = nodes
@property @property
def entry_point(self) -> "Node": def entry_point(self) -> Optional["Node"]:
""" """
Node: Entry point of the function Node: Entry point of the function
""" """

@ -104,7 +104,7 @@ class SolidityVariable(Context):
# dev function, will be removed once the code is stable # dev function, will be removed once the code is stable
def _check_name(self, name: str): # pylint: disable=no-self-use def _check_name(self, name: str): # pylint: disable=no-self-use
assert name in SOLIDITY_VARIABLES or name.endswith("_slot") or name.endswith("_offset") assert name in SOLIDITY_VARIABLES or name.endswith(("_slot", "_offset"))
@property @property
def state_variable(self): def state_variable(self):

@ -4,6 +4,7 @@
import json import json
import logging import logging
import os import os
import pathlib
import posixpath import posixpath
import re import re
from typing import Optional, Dict, List, Set, Union from typing import Optional, Dict, List, Set, Union
@ -218,8 +219,12 @@ class SlitherCore(Context):
for elem in r["elements"] for elem in r["elements"]
if "source_mapping" in elem if "source_mapping" in elem
] ]
source_mapping_elements = map(
lambda x: posixpath.normpath(x) if x else x, source_mapping_elements # Use POSIX-style paths so that filter_paths works across different
# OSes. Convert to a list so elements don't get consumed and are lost
# while evaluating the first pattern
source_mapping_elements = list(
map(lambda x: pathlib.Path(x).resolve().as_posix() if x else x, source_mapping_elements)
) )
matching = False matching = False

@ -39,9 +39,17 @@ class ArrayType(Type):
return self._length return self._length
@property @property
def lenght_value(self) -> Optional[Literal]: def length_value(self) -> Optional[Literal]:
return self._length_value return self._length_value
@property
def is_fixed_array(self) -> bool:
return bool(self.length)
@property
def is_dynamic_array(self) -> bool:
return not self.is_fixed_array
@property @property
def storage_size(self) -> Tuple[int, bool]: def storage_size(self) -> Tuple[int, bool]:
if self._length_value: if self._length_value:

@ -127,10 +127,10 @@ Max_Byte = {k: 2 ** (8 * (i + 1)) - 1 for i, k in enumerate(Byte[2:])}
Max_Byte["bytes"] = None Max_Byte["bytes"] = None
Max_Byte["string"] = None Max_Byte["string"] = None
Max_Byte["byte"] = 255 Max_Byte["byte"] = 255
Min_Byte = {k: 1 << (4 + 8 * i) for i, k in enumerate(Byte[2:])} Min_Byte = {k: 0 for k in Byte}
Min_Byte["bytes"] = 0x0 Min_Byte["bytes"] = 0x0
Min_Byte["string"] = None Min_Byte["string"] = 0x0
Min_Byte["byte"] = 0x10 Min_Byte["byte"] = 0x0
MaxValues = dict(dict(Max_Int, **Max_Uint), **Max_Byte) MaxValues = dict(dict(Max_Int, **Max_Uint), **Max_Byte)
MinValues = dict(dict(Min_Int, **Min_Uint), **Min_Byte) MinValues = dict(dict(Min_Int, **Min_Uint), **Min_Byte)
@ -193,7 +193,7 @@ class ElementaryType(Type):
if t == "address": if t == "address":
return int(160) return int(160)
if t.startswith("bytes") and t != "bytes": if t.startswith("bytes") and t != "bytes":
return int(t[len("bytes") :]) return int(t[len("bytes") :]) * 8
return None return None
@property @property

@ -13,8 +13,9 @@ class TypeInformation(Type):
def __init__(self, c): def __init__(self, c):
# pylint: disable=import-outside-toplevel # pylint: disable=import-outside-toplevel
from slither.core.declarations.contract import Contract from slither.core.declarations.contract import Contract
from slither.core.declarations.enum import Enum
assert isinstance(c, (Contract, ElementaryType)) assert isinstance(c, (Contract, ElementaryType, Enum))
super().__init__() super().__init__()
self._type = c self._type = c

@ -6,7 +6,9 @@ from .variables.uninitialized_local_variables import UninitializedLocalVars
from .attributes.constant_pragma import ConstantPragma from .attributes.constant_pragma import ConstantPragma
from .attributes.incorrect_solc import IncorrectSolc from .attributes.incorrect_solc import IncorrectSolc
from .attributes.locked_ether import LockedEther from .attributes.locked_ether import LockedEther
from .functions.arbitrary_send import ArbitrarySend from .functions.arbitrary_send_eth import ArbitrarySendEth
from .erc.erc20.arbitrary_send_erc20_no_permit import ArbitrarySendErc20NoPermit
from .erc.erc20.arbitrary_send_erc20_permit import ArbitrarySendErc20Permit
from .functions.suicidal import Suicidal from .functions.suicidal import Suicidal
# from .functions.complex_function import ComplexFunction # from .functions.complex_function import ComplexFunction
@ -34,7 +36,7 @@ from .shadowing.builtin_symbols import BuiltinSymbolShadowing
from .operations.block_timestamp import Timestamp from .operations.block_timestamp import Timestamp
from .statements.calls_in_loop import MultipleCallsInLoop from .statements.calls_in_loop import MultipleCallsInLoop
from .statements.incorrect_strict_equality import IncorrectStrictEquality from .statements.incorrect_strict_equality import IncorrectStrictEquality
from .erc.incorrect_erc20_interface import IncorrectERC20InterfaceDetection from .erc.erc20.incorrect_erc20_interface import IncorrectERC20InterfaceDetection
from .erc.incorrect_erc721_interface import IncorrectERC721InterfaceDetection from .erc.incorrect_erc721_interface import IncorrectERC721InterfaceDetection
from .erc.unindexed_event_parameters import UnindexedERC20EventParameters from .erc.unindexed_event_parameters import UnindexedERC20EventParameters
from .statements.deprecated_calls import DeprecatedStandards from .statements.deprecated_calls import DeprecatedStandards

@ -13,7 +13,9 @@ class ShiftParameterMixup(AbstractDetector):
IMPACT = DetectorClassification.HIGH IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.HIGH CONFIDENCE = DetectorClassification.HIGH
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#shift-parameter-mixup" WIKI = (
"https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-shift-in-assembly"
)
WIKI_TITLE = "Incorrect shift in assembly." WIKI_TITLE = "Incorrect shift in assembly."
WIKI_DESCRIPTION = "Detect if the values in a shift operation are reversed" WIKI_DESCRIPTION = "Detect if the values in a shift operation are reversed"

@ -14,7 +14,7 @@ from slither.formatters.attributes.incorrect_solc import custom_format
# 4: version number # 4: version number
# pylint: disable=anomalous-backslash-in-string # pylint: disable=anomalous-backslash-in-string
PATTERN = re.compile("(\^|>|>=|<|<=)?([ ]+)?(\d+)\.(\d+)\.(\d+)") PATTERN = re.compile(r"(\^|>|>=|<|<=)?([ ]+)?(\d+)\.(\d+)\.(\d+)")
class IncorrectSolc(AbstractDetector): class IncorrectSolc(AbstractDetector):

@ -0,0 +1,95 @@
from typing import List
from slither.core.cfg.node import Node
from slither.core.declarations.solidity_variables import SolidityVariable
from slither.slithir.operations import HighLevelCall, LibraryCall
from slither.core.declarations import Contract, Function, SolidityVariableComposed
from slither.analyses.data_dependency.data_dependency import is_dependent
from slither.core.compilation_unit import SlitherCompilationUnit
class ArbitrarySendErc20:
"""Detects instances where ERC20 can be sent from an arbitrary from address."""
def __init__(self, compilation_unit: SlitherCompilationUnit):
self._compilation_unit = compilation_unit
self._no_permit_results: List[Node] = []
self._permit_results: List[Node] = []
@property
def compilation_unit(self) -> SlitherCompilationUnit:
return self._compilation_unit
@property
def no_permit_results(self) -> List[Node]:
return self._no_permit_results
@property
def permit_results(self) -> List[Node]:
return self._permit_results
def _detect_arbitrary_from(self, contract: Contract):
for f in contract.functions:
all_high_level_calls = [
f_called[1].solidity_signature
for f_called in f.high_level_calls
if isinstance(f_called[1], Function)
]
all_library_calls = [f_called[1].solidity_signature for f_called in f.library_calls]
if (
"transferFrom(address,address,uint256)" in all_high_level_calls
or "safeTransferFrom(address,address,address,uint256)" in all_library_calls
):
if (
"permit(address,address,uint256,uint256,uint8,bytes32,bytes32)"
in all_high_level_calls
):
ArbitrarySendErc20._arbitrary_from(f.nodes, self._permit_results)
else:
ArbitrarySendErc20._arbitrary_from(f.nodes, self._no_permit_results)
@staticmethod
def _arbitrary_from(nodes: List[Node], results: List[Node]):
"""Finds instances of (safe)transferFrom that do not use msg.sender or address(this) as from parameter."""
for node in nodes:
for ir in node.irs:
if (
isinstance(ir, HighLevelCall)
and isinstance(ir.function, Function)
and ir.function.solidity_signature == "transferFrom(address,address,uint256)"
and not (
is_dependent(
ir.arguments[0],
SolidityVariableComposed("msg.sender"),
node.function.contract,
)
or is_dependent(
ir.arguments[0],
SolidityVariable("this"),
node.function.contract,
)
)
):
results.append(ir.node)
elif (
isinstance(ir, LibraryCall)
and ir.function.solidity_signature
== "safeTransferFrom(address,address,address,uint256)"
and not (
is_dependent(
ir.arguments[1],
SolidityVariableComposed("msg.sender"),
node.function.contract,
)
or is_dependent(
ir.arguments[1],
SolidityVariable("this"),
node.function.contract,
)
)
):
results.append(ir.node)
def detect(self):
"""Detect transfers that use arbitrary `from` parameter."""
for c in self.compilation_unit.contracts_derived:
self._detect_arbitrary_from(c)

@ -0,0 +1,45 @@
from typing import List
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.utils.output import Output
from .arbitrary_send_erc20 import ArbitrarySendErc20
class ArbitrarySendErc20NoPermit(AbstractDetector):
"""
Detect when `msg.sender` is not used as `from` in transferFrom
"""
ARGUMENT = "arbitrary-send-erc20"
HELP = "transferFrom uses arbitrary `from`"
IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.HIGH
WIKI = "https://github.com/trailofbits/slither/wiki/Detector-Documentation#arbitrary-send-erc20"
WIKI_TITLE = "Arbitrary `from` in transferFrom"
WIKI_DESCRIPTION = "Detect when `msg.sender` is not used as `from` in transferFrom."
WIKI_EXPLOIT_SCENARIO = """
```solidity
function a(address from, address to, uint256 amount) public {
erc20.transferFrom(from, to, am);
}
```
Alice approves this contract to spend her ERC20 tokens. Bob can call `a` and specify Alice's address as the `from` parameter in `transferFrom`, allowing him to transfer Alice's tokens to himself."""
WIKI_RECOMMENDATION = """
Use `msg.sender` as `from` in transferFrom.
"""
def _detect(self) -> List[Output]:
""""""
results: List[Output] = []
arbitrary_sends = ArbitrarySendErc20(self.compilation_unit)
arbitrary_sends.detect()
for node in arbitrary_sends.no_permit_results:
func = node.function
info = [func, " uses arbitrary from in transferFrom: ", node, "\n"]
res = self.generate_result(info)
results.append(res)
return results

@ -0,0 +1,53 @@
from typing import List
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.utils.output import Output
from .arbitrary_send_erc20 import ArbitrarySendErc20
class ArbitrarySendErc20Permit(AbstractDetector):
"""
Detect when `msg.sender` is not used as `from` in transferFrom along with the use of permit.
"""
ARGUMENT = "arbitrary-send-erc20-permit"
HELP = "transferFrom uses arbitrary from with permit"
IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.MEDIUM
WIKI = "https://github.com/trailofbits/slither/wiki/Detector-Documentation#arbitrary-send-erc20-permit"
WIKI_TITLE = "Arbitrary `from` in transferFrom used with permit"
WIKI_DESCRIPTION = (
"Detect when `msg.sender` is not used as `from` in transferFrom and permit is used."
)
WIKI_EXPLOIT_SCENARIO = """
```solidity
function bad(address from, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s, address to) public {
erc20.permit(from, address(this), value, deadline, v, r, s);
erc20.transferFrom(from, to, value);
}
```
If an ERC20 token does not implement permit and has a fallback function e.g. WETH, transferFrom allows an attacker to transfer all tokens approved for this contract."""
WIKI_RECOMMENDATION = """
Ensure that the underlying ERC20 token correctly implements a permit function.
"""
def _detect(self) -> List[Output]:
""""""
results: List[Output] = []
arbitrary_sends = ArbitrarySendErc20(self.compilation_unit)
arbitrary_sends.detect()
for node in arbitrary_sends.permit_results:
func = node.function
info = [
func,
" uses arbitrary from in transferFrom in combination with permit: ",
node,
"\n",
]
res = self.generate_result(info)
results.append(res)
return results

@ -90,8 +90,8 @@ def detect_arbitrary_send(contract: Contract):
return ret return ret
class ArbitrarySend(AbstractDetector): class ArbitrarySendEth(AbstractDetector):
ARGUMENT = "arbitrary-send" ARGUMENT = "arbitrary-send-eth"
HELP = "Functions that send Ether to arbitrary destinations" HELP = "Functions that send Ether to arbitrary destinations"
IMPACT = DetectorClassification.HIGH IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.MEDIUM CONFIDENCE = DetectorClassification.MEDIUM
@ -104,7 +104,7 @@ class ArbitrarySend(AbstractDetector):
# region wiki_exploit_scenario # region wiki_exploit_scenario
WIKI_EXPLOIT_SCENARIO = """ WIKI_EXPLOIT_SCENARIO = """
```solidity ```solidity
contract ArbitrarySend{ contract ArbitrarySendEth{
address destination; address destination;
function setDestination(){ function setDestination(){
destination = msg.sender; destination = msg.sender;

@ -94,7 +94,7 @@ Solidity defines a [naming convention](https://solidity.readthedocs.io/en/v0.4.2
"private", "private",
] and self.is_mixed_case_with_underscore(func.name): ] and self.is_mixed_case_with_underscore(func.name):
continue continue
if func.name.startswith("echidna_") or func.name.startswith("crytic_"): if func.name.startswith(("echidna_", "crytic_")):
continue continue
info = ["Function ", func, " is not in mixedCase\n"] info = ["Function ", func, " is not in mixedCase\n"]

@ -1,4 +1,4 @@
from typing import List from typing import List, Optional
from slither.core.cfg.node import NodeType, Node from slither.core.cfg.node import NodeType, Node
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.core.declarations import Contract from slither.core.declarations import Contract
@ -22,7 +22,11 @@ def detect_call_in_loop(contract: Contract) -> List[Node]:
return ret return ret
def call_in_loop(node: Node, in_loop_counter: int, visited: List[Node], ret: List[Node]) -> None: def call_in_loop(
node: Optional[Node], in_loop_counter: int, visited: List[Node], ret: List[Node]
) -> None:
if node is None:
return
if node in visited: if node in visited:
return return
# shared visited # shared visited

@ -1,4 +1,4 @@
from typing import List from typing import List, Optional
from slither.core.cfg.node import NodeType, Node from slither.core.cfg.node import NodeType, Node
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.core.declarations import Contract from slither.core.declarations import Contract
@ -17,8 +17,12 @@ def detect_costly_operations_in_loop(contract: Contract) -> List[Node]:
def costly_operations_in_loop( def costly_operations_in_loop(
node: Node, in_loop_counter: int, visited: List[Node], ret: List[Node] node: Optional[Node], in_loop_counter: int, visited: List[Node], ret: List[Node]
) -> None: ) -> None:
if node is None:
return
if node in visited: if node in visited:
return return
# shared visited # shared visited

@ -1,4 +1,4 @@
from typing import List from typing import List, Optional
from slither.core.cfg.node import NodeType, Node from slither.core.cfg.node import NodeType, Node
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.slithir.operations import LowLevelCall, InternalCall from slither.slithir.operations import LowLevelCall, InternalCall
@ -15,8 +15,12 @@ def detect_delegatecall_in_loop(contract: Contract) -> List[Node]:
def delegatecall_in_loop( def delegatecall_in_loop(
node: Node, in_loop_counter: int, visited: List[Node], results: List[Node] node: Optional[Node], in_loop_counter: int, visited: List[Node], results: List[Node]
) -> None: ) -> None:
if node is None:
return
if node in visited: if node in visited:
return return
# shared visited # shared visited

@ -1,4 +1,4 @@
from typing import List from typing import List, Optional
from slither.core.cfg.node import NodeType, Node from slither.core.cfg.node import NodeType, Node
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.slithir.operations import InternalCall from slither.slithir.operations import InternalCall
@ -15,8 +15,12 @@ def detect_msg_value_in_loop(contract: Contract) -> List[Node]:
def msg_value_in_loop( def msg_value_in_loop(
node: Node, in_loop_counter: int, visited: List[Node], results: List[Node] node: Optional[Node], in_loop_counter: int, visited: List[Node], results: List[Node]
) -> None: ) -> None:
if node is None:
return
if node in visited: if node in visited:
return return
# shared visited # shared visited

@ -2,9 +2,19 @@
Module detecting numbers with too many digits. Module detecting numbers with too many digits.
""" """
import re
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.slithir.variables import Constant from slither.slithir.variables import Constant
_HEX_ADDRESS_REGEXP = re.compile("(0[xX])?[0-9a-fA-F]{40}")
def is_hex_address(value) -> bool:
"""
Checks if the given string of text type is an address in hexadecimal encoded form.
"""
return _HEX_ADDRESS_REGEXP.fullmatch(value) is not None
class TooManyDigits(AbstractDetector): class TooManyDigits(AbstractDetector):
""" """
@ -58,7 +68,7 @@ Use:
if isinstance(read, Constant): if isinstance(read, Constant):
# read.value can return an int or a str. Convert it to str # read.value can return an int or a str. Convert it to str
value_as_str = read.original_value value_as_str = read.original_value
if "00000" in value_as_str: if "00000" in value_as_str and not is_hex_address(value_as_str):
# Info to be printed # Info to be printed
ret.append(node) ret.append(node)
return ret return ret

@ -102,7 +102,7 @@ class UnprotectedUpgradeable(AbstractDetector):
info = ( info = (
[ [
contract, contract,
" is an upgradeable contract that does not protect its initiliaze functions: ", " is an upgradeable contract that does not protect its initialize functions: ",
] ]
+ initialize_functions + initialize_functions
+ [ + [

@ -17,7 +17,9 @@ class SimilarVarsDetection(AbstractDetector):
IMPACT = DetectorClassification.INFORMATIONAL IMPACT = DetectorClassification.INFORMATIONAL
CONFIDENCE = DetectorClassification.MEDIUM CONFIDENCE = DetectorClassification.MEDIUM
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#variable-names-are-too-similar" WIKI = (
"https://github.com/crytic/slither/wiki/Detector-Documentation#variable-names-too-similar"
)
WIKI_TITLE = "Variable names too similar" WIKI_TITLE = "Variable names too similar"
WIKI_DESCRIPTION = "Detect variables with names that are too similar." WIKI_DESCRIPTION = "Detect variables with names that are too similar."

@ -88,7 +88,11 @@ Bob calls `transfer`. As a result, all Ether is sent to the address `0x0` and is
for contract in self.compilation_unit.contracts: for contract in self.compilation_unit.contracts:
for function in contract.functions: for function in contract.functions:
if function.is_implemented and function.contract_declarer == contract: if (
function.is_implemented
and function.contract_declarer == contract
and function.entry_point
):
if function.contains_assembly: if function.contains_assembly:
continue continue
# dont consider storage variable, as they are detected by another detector # dont consider storage variable, as they are detected by another detector

@ -96,7 +96,7 @@ Bob calls `func`. As a result, `owner` is overridden to `0`.
for contract in self.compilation_unit.contracts: for contract in self.compilation_unit.contracts:
for function in contract.functions: for function in contract.functions:
if function.is_implemented: if function.is_implemented and function.entry_point:
uninitialized_storage_variables = [ uninitialized_storage_variables = [
v for v in function.local_variables if v.is_storage and v.uninitialized v for v in function.local_variables if v.is_storage and v.uninitialized
] ]

@ -13,7 +13,7 @@ REPLACEMENT_VERSIONS = ["^0.4.25", "^0.5.3"]
# 2: version number # 2: version number
# 3: version number # 3: version number
# 4: version number # 4: version number
PATTERN = re.compile("(\^|>|>=|<|<=)?([ ]+)?(\d+)\.(\d+)\.(\d+)") PATTERN = re.compile(r"(\^|>|>=|<|<=)?([ ]+)?(\d+)\.(\d+)\.(\d+)")
def custom_format(slither, result): def custom_format(slither, result):

@ -13,8 +13,7 @@ REPLACEMENT_VERSIONS = ["^0.4.25", "^0.5.3"]
# 3: version number # 3: version number
# 4: version number # 4: version number
# pylint: disable=anomalous-backslash-in-string PATTERN = re.compile(r"(\^|>|>=|<|<=)?([ ]+)?(\d+)\.(\d+)\.(\d+)")
PATTERN = re.compile("(\^|>|>=|<|<=)?([ ]+)?(\d+)\.(\d+)\.(\d+)")
def custom_format(slither, result): def custom_format(slither, result):

@ -300,10 +300,10 @@ def _patch(compilation_unit: SlitherCompilationUnit, result, element, _target):
# group 2: beginning of the to type # group 2: beginning of the to type
# nested mapping are within the group 1 # nested mapping are within the group 1
# RE_MAPPING = '[ ]*mapping[ ]*\([ ]*([\=\>\(\) a-zA-Z0-9\._\[\]]*)[ ]*=>[ ]*([a-zA-Z0-9\._\[\]]*)\)' # RE_MAPPING = '[ ]*mapping[ ]*\([ ]*([\=\>\(\) a-zA-Z0-9\._\[\]]*)[ ]*=>[ ]*([a-zA-Z0-9\._\[\]]*)\)'
RE_MAPPING_FROM = b"([a-zA-Z0-9\._\[\]]*)" RE_MAPPING_FROM = rb"([a-zA-Z0-9\._\[\]]*)"
RE_MAPPING_TO = b"([\=\>\(\) a-zA-Z0-9\._\[\]\ ]*)" RE_MAPPING_TO = rb"([\=\>\(\) a-zA-Z0-9\._\[\]\ ]*)"
RE_MAPPING = ( RE_MAPPING = (
b"[ ]*mapping[ ]*\([ ]*" + RE_MAPPING_FROM + b"[ ]*" + b"=>" + b"[ ]*" + RE_MAPPING_TO + b"\)" rb"[ ]*mapping[ ]*\([ ]*" + RE_MAPPING_FROM + b"[ ]*" + b"=>" + b"[ ]*" + RE_MAPPING_TO + rb"\)"
) )

@ -28,7 +28,7 @@ class VariableOrder(AbstractPrinter):
txt += f"\n{contract.name}:\n" txt += f"\n{contract.name}:\n"
table = MyPrettyTable(["Name", "Type", "Slot", "Offset"]) table = MyPrettyTable(["Name", "Type", "Slot", "Offset"])
for variable in contract.state_variables_ordered: for variable in contract.state_variables_ordered:
if not variable.is_constant: if not variable.is_constant and not variable.is_immutable:
slot, offset = contract.compilation_unit.storage_layout_of(contract, variable) slot, offset = contract.compilation_unit.storage_layout_of(contract, variable)
table.add_row([variable.canonical_name, str(variable.type), slot, offset]) table.add_row([variable.canonical_name, str(variable.type), slot, offset])

@ -53,7 +53,7 @@ class Slither(SlitherCore): # pylint: disable=too-many-instance-attributes
Keyword Args: Keyword Args:
solc (str): solc binary location (default 'solc') solc (str): solc binary location (default 'solc')
disable_solc_warnings (bool): True to disable solc warnings (default false) disable_solc_warnings (bool): True to disable solc warnings (default false)
solc_arguments (str): solc arguments (default '') solc_args (str): solc arguments (default '')
ast_format (str): ast format (default '--ast-compact-json') ast_format (str): ast format (default '--ast-compact-json')
filter_paths (list(str)): list of path to filter (default []) filter_paths (list(str)): list of path to filter (default [])
triage_mode (bool): if true, switch to triage mode (default false) triage_mode (bool): if true, switch to triage mode (default false)

@ -33,6 +33,7 @@ from slither.core.solidity_types.elementary_type import (
MaxValues, MaxValues,
) )
from slither.core.solidity_types.type import Type from slither.core.solidity_types.type import Type
from slither.core.solidity_types.type_alias import TypeAlias
from slither.core.variables.function_type_variable import FunctionTypeVariable from slither.core.variables.function_type_variable import FunctionTypeVariable
from slither.core.variables.state_variable import StateVariable from slither.core.variables.state_variable import StateVariable
from slither.core.variables.variable import Variable from slither.core.variables.variable import Variable
@ -399,6 +400,13 @@ def propagate_type_and_convert_call(result, node):
ins = new_ins ins = new_ins
result[idx] = ins result[idx] = ins
# If there's two consecutive type conversions, keep only the last
# ARG_CALL x call_data = [x]
# TMP_4 = CONVERT x to Fix call_data = []
# TMP_5 = CONVERT TMP_4 to uint192 call_data = [TMP_5]
if isinstance(ins, TypeConversion) and ins.variable in call_data:
call_data.remove(ins.variable)
if isinstance(ins, Argument): if isinstance(ins, Argument):
# In case of dupplicate arguments we overwrite the value # In case of dupplicate arguments we overwrite the value
# This can happen because of addr.call.value(1).value(2) # This can happen because of addr.call.value(1).value(2)
@ -531,7 +539,9 @@ def propagate_types(ir, node: "Node"): # pylint: disable=too-many-locals
return convert_type_of_high_and_internal_level_call(ir, contract) return convert_type_of_high_and_internal_level_call(ir, contract)
# Convert HighLevelCall to LowLevelCall # Convert HighLevelCall to LowLevelCall
if isinstance(t, ElementaryType) and t.name == "address": if (isinstance(t, ElementaryType) and t.name == "address") or (
isinstance(t, TypeAlias) and t.underlying_type.name == "address"
):
# Cannot be a top level function with this. # Cannot be a top level function with this.
assert isinstance(node_function, FunctionContract) assert isinstance(node_function, FunctionContract)
if ir.destination.name == "this": if ir.destination.name == "this":
@ -817,12 +827,28 @@ def extract_tmp_call(ins: TmpCall, contract: Optional[Contract]): # pylint: dis
# lib L { event E()} # lib L { event E()}
# ... # ...
# emit L.E(); # emit L.E();
if str(ins.ori.variable_right) in [f.name for f in ins.ori.variable_left.events]: if str(ins.ori.variable_right) in ins.ori.variable_left.events_as_dict:
eventcall = EventCall(ins.ori.variable_right) eventcall = EventCall(ins.ori.variable_right)
eventcall.set_expression(ins.expression) eventcall.set_expression(ins.expression)
eventcall.call_id = ins.call_id eventcall.call_id = ins.call_id
return eventcall return eventcall
# lib Lib { error Error()} ... revert Lib.Error()
if str(ins.ori.variable_right) in ins.ori.variable_left.custom_errors_as_dict:
custom_error = ins.ori.variable_left.custom_errors_as_dict[
str(ins.ori.variable_right)
]
assert isinstance(
custom_error,
CustomError,
)
sol_function = SolidityCustomRevert(custom_error)
solidity_call = SolidityCall(
sol_function, ins.nbr_arguments, ins.lvalue, ins.type_call
)
solidity_call.set_expression(ins.expression)
return solidity_call
libcall = LibraryCall( libcall = LibraryCall(
ins.ori.variable_left, ins.ori.variable_left,
ins.ori.variable_right, ins.ori.variable_right,
@ -1369,7 +1395,11 @@ def convert_type_library_call(ir: HighLevelCall, lib_contract: Contract):
if len(candidates) == 1: if len(candidates) == 1:
func = candidates[0] func = candidates[0]
if func is None: # We can discard if there are arguments here because libraries only support constant variables
# And constant variables cannot have non-value type
# i.e. "uint[2] constant arr = [1,2];" is not possible in Solidity
# If this were to change, the following condition might be broken
if func is None and not ir.arguments:
# TODO: handle collision with multiple state variables/functions # TODO: handle collision with multiple state variables/functions
func = lib_contract.get_state_variable_from_name(ir.function_name) func = lib_contract.get_state_variable_from_name(ir.function_name)
if func is None and candidates: if func is None and candidates:
@ -1657,7 +1687,10 @@ def convert_constant_types(irs):
if isinstance(ir.rvalue, TupleVariable): if isinstance(ir.rvalue, TupleVariable):
# TODO: fix missing Unpack conversion # TODO: fix missing Unpack conversion
continue continue
if ir.rvalue.type.type not in ElementaryTypeInt: if isinstance(ir.rvalue.type, TypeAlias):
ir.rvalue.set_type(ElementaryType(ir.lvalue.type.name))
was_changed = True
elif ir.rvalue.type.type not in ElementaryTypeInt:
ir.rvalue.set_type(ElementaryType(ir.lvalue.type.type)) ir.rvalue.set_type(ElementaryType(ir.lvalue.type.type))
was_changed = True was_changed = True
if isinstance(ir, Binary): if isinstance(ir, Binary):
@ -1676,6 +1709,7 @@ def convert_constant_types(irs):
# TODO: add POP instruction # TODO: add POP instruction
break break
types = [p.type for p in func.parameters] types = [p.type for p in func.parameters]
assert len(types) == len(ir.arguments)
for idx, arg in enumerate(ir.arguments): for idx, arg in enumerate(ir.arguments):
t = types[idx] t = types[idx]
if isinstance(t, ElementaryType): if isinstance(t, ElementaryType):

@ -157,7 +157,10 @@ class ContractSolc(CallerContextExpression):
if "contractKind" in attributes: if "contractKind" in attributes:
if attributes["contractKind"] == "interface": if attributes["contractKind"] == "interface":
self._contract.is_interface = True self._contract.is_interface = True
self._contract.kind = attributes["contractKind"] elif attributes["contractKind"] == "library":
self._contract.is_library = True
self._contract.contract_kind = attributes["contractKind"]
self._linearized_base_contracts = attributes["linearizedBaseContracts"] self._linearized_base_contracts = attributes["linearizedBaseContracts"]
# self._contract.fullyImplemented = attributes["fullyImplemented"] # self._contract.fullyImplemented = attributes["fullyImplemented"]

@ -445,7 +445,7 @@ def parse_expression(expression: Dict, caller_context: CallerContextExpression)
t = expression["attributes"]["type"] t = expression["attributes"]["type"]
if t: if t:
found = re.findall("[struct|enum|function|modifier] \(([\[\] ()a-zA-Z0-9\.,_]*)\)", t) found = re.findall(r"[struct|enum|function|modifier] \(([\[\] ()a-zA-Z0-9\.,_]*)\)", t)
assert len(found) <= 1 assert len(found) <= 1
if found: if found:
value = value + "(" + found[0] + ")" value = value + "(" + found[0] + ")"
@ -455,7 +455,6 @@ def parse_expression(expression: Dict, caller_context: CallerContextExpression)
referenced_declaration = expression["referencedDeclaration"] referenced_declaration = expression["referencedDeclaration"]
else: else:
referenced_declaration = None referenced_declaration = None
var, was_created = find_variable(value, caller_context, referenced_declaration) var, was_created = find_variable(value, caller_context, referenced_declaration)
if was_created: if was_created:
var.set_offset(src, caller_context.compilation_unit) var.set_offset(src, caller_context.compilation_unit)

@ -401,7 +401,7 @@ Please rename it, this name is reserved for Slither's internals"""
father_constructors = [] father_constructors = []
# try: # try:
# Resolve linearized base contracts. # Resolve linearized base contracts.
missing_inheritance = False missing_inheritance = None
for i in contract_parser.linearized_base_contracts[1:]: for i in contract_parser.linearized_base_contracts[1:]:
if i in contract_parser.remapping: if i in contract_parser.remapping:
@ -418,7 +418,7 @@ Please rename it, this name is reserved for Slither's internals"""
elif i in self._contracts_by_id: elif i in self._contracts_by_id:
ancestors.append(self._contracts_by_id[i]) ancestors.append(self._contracts_by_id[i])
else: else:
missing_inheritance = True missing_inheritance = i
# Resolve immediate base contracts # Resolve immediate base contracts
for i in contract_parser.baseContracts: for i in contract_parser.baseContracts:
@ -432,7 +432,7 @@ Please rename it, this name is reserved for Slither's internals"""
elif i in self._contracts_by_id: elif i in self._contracts_by_id:
fathers.append(self._contracts_by_id[i]) fathers.append(self._contracts_by_id[i])
else: else:
missing_inheritance = True missing_inheritance = i
# Resolve immediate base constructor calls # Resolve immediate base constructor calls
for i in contract_parser.baseConstructorContractsCalled: for i in contract_parser.baseConstructorContractsCalled:
@ -446,7 +446,7 @@ Please rename it, this name is reserved for Slither's internals"""
elif i in self._contracts_by_id: elif i in self._contracts_by_id:
father_constructors.append(self._contracts_by_id[i]) father_constructors.append(self._contracts_by_id[i])
else: else:
missing_inheritance = True missing_inheritance = i
contract_parser.underlying_contract.set_inheritance( contract_parser.underlying_contract.set_inheritance(
ancestors, fathers, father_constructors ancestors, fathers, father_constructors
@ -456,7 +456,14 @@ Please rename it, this name is reserved for Slither's internals"""
self._compilation_unit.contracts_with_missing_inheritance.add( self._compilation_unit.contracts_with_missing_inheritance.add(
contract_parser.underlying_contract contract_parser.underlying_contract
) )
contract_parser.log_incorrect_parsing(f"Missing inheritance {contract_parser}") txt = f"Missing inheritance {contract_parser.underlying_contract} ({contract_parser.compilation_unit.crytic_compile_compilation_unit.unique_id})\n"
txt += f"Missing inheritance ID: {missing_inheritance}\n"
if contract_parser.underlying_contract.inheritance:
txt += "Inheritance found:\n"
for contract_inherited in contract_parser.underlying_contract.inheritance:
txt += f"\t - {contract_inherited} (ID {contract_inherited.id})\n"
contract_parser.log_incorrect_parsing(txt)
contract_parser.set_is_analyzed(True) contract_parser.set_is_analyzed(True)
contract_parser.delete_content() contract_parser.delete_content()
@ -679,12 +686,12 @@ Please rename it, this name is reserved for Slither's internals"""
for func in contract.functions + contract.modifiers: for func in contract.functions + contract.modifiers:
try: try:
func.generate_slithir_and_analyze() func.generate_slithir_and_analyze()
except AttributeError: except AttributeError as e:
# This can happens for example if there is a call to an interface # This can happens for example if there is a call to an interface
# And the interface is redefined due to contract's name reuse # And the interface is redefined due to contract's name reuse
# But the available version misses some functions # But the available version misses some functions
self._underlying_contract_to_parser[contract].log_incorrect_parsing( self._underlying_contract_to_parser[contract].log_incorrect_parsing(
f"Impossible to generate IR for {contract.name}.{func.name}" f"Impossible to generate IR for {contract.name}.{func.name}:\n {e}"
) )
contract.convert_expression_to_slithir_ssa() contract.convert_expression_to_slithir_ssa()

@ -112,7 +112,7 @@ def _find_from_type_name( # pylint: disable=too-many-locals,too-many-branches,t
if not var_type: if not var_type:
if name.startswith("function "): if name.startswith("function "):
found = re.findall( found = re.findall(
"function \(([ ()\[\]a-zA-Z0-9\.,]*?)\)(?: payable)?(?: (?:external|internal|pure|view))?(?: returns \(([a-zA-Z0-9() \.,]*)\))?", r"function \(([ ()\[\]a-zA-Z0-9\.,]*?)\)(?: payable)?(?: (?:external|internal|pure|view))?(?: returns \(([a-zA-Z0-9() \.,]*)\))?",
name, name,
) )
assert len(found) == 1 assert len(found) == 1
@ -159,10 +159,10 @@ def _find_from_type_name( # pylint: disable=too-many-locals,too-many-branches,t
if name.startswith("mapping("): if name.startswith("mapping("):
# nested mapping declared with var # nested mapping declared with var
if name.count("mapping(") == 1: if name.count("mapping(") == 1:
found = re.findall("mapping\(([a-zA-Z0-9\.]*) => ([ a-zA-Z0-9\.\[\]]*)\)", name) found = re.findall(r"mapping\(([a-zA-Z0-9\.]*) => ([ a-zA-Z0-9\.\[\]]*)\)", name)
else: else:
found = re.findall( found = re.findall(
"mapping\(([a-zA-Z0-9\.]*) => (mapping\([=> a-zA-Z0-9\.\[\]]*\))\)", r"mapping\(([a-zA-Z0-9\.]*) => (mapping\([=> a-zA-Z0-9\.\[\]]*\))\)",
name, name,
) )
assert len(found) == 1 assert len(found) == 1
@ -272,8 +272,12 @@ def parse_type(
all_structuress = [c.structures for c in scope.contracts.values()] all_structuress = [c.structures for c in scope.contracts.values()]
all_structures = [item for sublist in all_structuress for item in sublist] all_structures = [item for sublist in all_structuress for item in sublist]
all_structures += structures_direct_access all_structures += structures_direct_access
enums_direct_access = [] enums_direct_access = []
all_enums = scope.enums.values() all_enumss = [c.enums for c in scope.contracts.values()]
all_enums = [item for sublist in all_enumss for item in sublist]
all_enums += scope.enums.values()
contracts = scope.contracts.values() contracts = scope.contracts.values()
functions = list(scope.functions) functions = list(scope.functions)

@ -737,7 +737,7 @@ def _parse_yul_magic_suffixes(name: str, root: YulScope) -> Optional[Expression]
# Currently SlithIR doesnt support raw access to memory # Currently SlithIR doesnt support raw access to memory
# So things like .offset/.slot will return the variable # So things like .offset/.slot will return the variable
# Instaed of the actual offset/slot # Instaed of the actual offset/slot
if name.endswith("_slot") or name.endswith(".slot"): if name.endswith(("_slot", ".slot")):
potential_name = name[:-5] potential_name = name[:-5]
variable_found = _check_for_state_variable_name(root, potential_name) variable_found = _check_for_state_variable_name(root, potential_name)
if variable_found: if variable_found:
@ -745,7 +745,7 @@ def _parse_yul_magic_suffixes(name: str, root: YulScope) -> Optional[Expression]
var = root.function.get_local_variable_from_name(potential_name) var = root.function.get_local_variable_from_name(potential_name)
if var and var.is_storage: if var and var.is_storage:
return Identifier(var) return Identifier(var)
if name.endswith("_offset") or name.endswith(".offset"): if name.endswith(("_offset", ".offset")):
potential_name = name[:-7] potential_name = name[:-7]
variable_found = _check_for_state_variable_name(root, potential_name) variable_found = _check_for_state_variable_name(root, potential_name)
if variable_found: if variable_found:

@ -192,6 +192,7 @@ def generic_erc_checks(contract, erc_functions, erc_events, ret, explored=None):
logger.info("## Check functions") logger.info("## Check functions")
for erc_function in erc_functions: for erc_function in erc_functions:
_check_signature(erc_function, contract, ret) _check_signature(erc_function, contract, ret)
if erc_events:
logger.info("\n## Check events") logger.info("\n## Check events")
for erc_event in erc_events: for erc_event in erc_events:
_check_events(erc_event, contract, ret) _check_events(erc_event, contract, ret)

@ -79,6 +79,12 @@ def parse_args():
action="store_true", action="store_true",
) )
group_patching.add_argument(
"--convert-library-to-internal",
help="Convert external or public functions to internal in library.",
action="store_true",
)
group_patching.add_argument( group_patching.add_argument(
"--remove-assert", help="Remove call to assert().", action="store_true" "--remove-assert", help="Remove call to assert().", action="store_true"
) )
@ -111,6 +117,7 @@ def main():
compilation_unit, compilation_unit,
external_to_public=args.convert_external, external_to_public=args.convert_external,
remove_assert=args.remove_assert, remove_assert=args.remove_assert,
convert_library_to_internal=args.convert_library_to_internal,
private_to_internal=args.convert_private, private_to_internal=args.convert_private,
export_path=args.dir, export_path=args.dir,
pragma_solidity=args.pragma_solidity, pragma_solidity=args.pragma_solidity,

@ -51,6 +51,7 @@ class Flattening:
compilation_unit: SlitherCompilationUnit, compilation_unit: SlitherCompilationUnit,
external_to_public=False, external_to_public=False,
remove_assert=False, remove_assert=False,
convert_library_to_internal=False,
private_to_internal=False, private_to_internal=False,
export_path: Optional[str] = None, export_path: Optional[str] = None,
pragma_solidity: Optional[str] = None, pragma_solidity: Optional[str] = None,
@ -61,6 +62,7 @@ class Flattening:
self._external_to_public = external_to_public self._external_to_public = external_to_public
self._remove_assert = remove_assert self._remove_assert = remove_assert
self._use_abi_encoder_v2 = False self._use_abi_encoder_v2 = False
self._convert_library_to_internal = convert_library_to_internal
self._private_to_internal = private_to_internal self._private_to_internal = private_to_internal
self._pragma_solidity = pragma_solidity self._pragma_solidity = pragma_solidity
@ -111,7 +113,7 @@ class Flattening:
to_patch = [] to_patch = []
# interface must use external # interface must use external
if self._external_to_public and contract.contract_kind != "interface": if self._external_to_public and not contract.is_interface:
for f in contract.functions_declared: for f in contract.functions_declared:
# fallback must be external # fallback must be external
if f.is_fallback or f.is_constructor_variables: if f.is_fallback or f.is_constructor_variables:
@ -146,6 +148,36 @@ class Flattening:
) )
) )
if self._convert_library_to_internal and contract.is_library:
for f in contract.functions_declared:
visibility = ""
if f.visibility in ["external", "public"]:
visibility = f.visibility
attributes_start = (
f.parameters_src().source_mapping["start"]
+ f.parameters_src().source_mapping["length"]
)
attributes_end = f.returns_src().source_mapping["start"]
attributes = content[attributes_start:attributes_end]
regex = (
re.search(r"((\sexternal)\s+)|(\sexternal)$|(\)external)$", attributes)
if visibility == "external"
else re.search(r"((\spublic)\s+)|(\spublic)$|(\)public)$", attributes)
)
if regex:
to_patch.append(
Patch(
attributes_start + regex.span()[0] + 1,
"external_to_internal"
if visibility == "external"
else "public_to_internal",
)
)
else:
raise SlitherException(
f"{visibility} keyword not found {f.name} {attributes}"
)
if self._private_to_internal: if self._private_to_internal:
for variable in contract.state_variables_declared: for variable in contract.state_variables_declared:
if variable.visibility == "private": if variable.visibility == "private":
@ -186,6 +218,10 @@ class Flattening:
index = index - start index = index - start
if patch_type == "public_to_external": if patch_type == "public_to_external":
content = content[:index] + "public" + content[index + len("external") :] content = content[:index] + "public" + content[index + len("external") :]
elif patch_type == "external_to_internal":
content = content[:index] + "internal" + content[index + len("external") :]
elif patch_type == "public_to_internal":
content = content[:index] + "internal" + content[index + len("public") :]
elif patch_type == "private_to_internal": elif patch_type == "private_to_internal":
content = content[:index] + "internal" + content[index + len("private") :] content = content[:index] + "internal" + content[index + len("private") :]
elif patch_type == "calldata_to_memory": elif patch_type == "calldata_to_memory":

@ -22,8 +22,8 @@ def _get_all_covered_kspec_functions(target: str) -> Set[Tuple[str, str]]:
# Create a set of our discovered functions which are covered # Create a set of our discovered functions which are covered
covered_functions: Set[Tuple[str, str]] = set() covered_functions: Set[Tuple[str, str]] = set()
BEHAVIOUR_PATTERN = re.compile("behaviour\s+(\S+)\s+of\s+(\S+)") BEHAVIOUR_PATTERN = re.compile(r"behaviour\s+(\S+)\s+of\s+(\S+)")
INTERFACE_PATTERN = re.compile("interface\s+([^\r\n]+)") INTERFACE_PATTERN = re.compile(r"interface\s+([^\r\n]+)")
# Read the file contents # Read the file contents
with open(target, "r", encoding="utf8") as target_file: with open(target, "r", encoding="utf8") as target_file:

@ -0,0 +1,91 @@
# Slither-read-storage
Slither-read-storage is a tool to retrieve the storage slots and values of entire contracts or single variables.
## Usage
### CLI Interface
```shell
positional arguments:
contract_source (DIR) ADDRESS The deployed contract address if verified on etherscan. Prepend project directory for unverified contracts.
optional arguments:
--variable-name VARIABLE_NAME The name of the variable whose value will be returned.
--rpc-url RPC_URL An endpoint for web3 requests.
--key KEY The key/ index whose value will be returned from a mapping or array.
--deep-key DEEP_KEY The key/ index whose value will be returned from a deep mapping or multidimensional array.
--struct-var STRUCT_VAR The name of the variable whose value will be returned from a struct.
--storage-address STORAGE_ADDRESS The address of the storage contract (if a proxy pattern is used).
--contract-name CONTRACT_NAME The name of the logic contract.
--layout Toggle used to write a JSON file with the entire storage layout.
--value Toggle used to include values in output.
--max-depth MAX_DEPTH Max depth to search in data structure.
```
### Examples
Retrieve the storage slots of a local contract:
```shell
slither-read-storage file.sol 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 --layout
```
Retrieve the storage slots of a contract verified on an Etherscan-like platform:
```shell
slither-read-storage 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 --layout
```
To retrieve the values as well, pass `--value` and `--rpc-url $RPC_URL`:
```shell
slither-read-storage 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 --layout --rpc-url $RPC_URL --value
```
To view only the slot of the `slot0` structure variable, pass `--variable-name slot0`:
```shell
slither-read-storage 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 --variable-name slot0 --rpc-url $RPC_URL --value
```
To view a member of the `slot0` struct, pass `--struct-var tick`
```shell
slither-read-storage 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 --variable-name slot0 --rpc-url $RPC_URL --value --struct-var tick
```
Retrieve the ERC20 balance slot of an account:
```shell
slither-read-storage 0xa2327a938Febf5FEC13baCFb16Ae10EcBc4cbDCF --variable-name balances --key 0xab5801a7d398351b8be11c439e05c5b3259aec9b
```
To retrieve the actual balance, pass `--variable-name balances` and `--key 0xab5801a7d398351b8be11c439e05c5b3259aec9b`. (`balances` is a `mapping(address => uint)`)
Since this contract uses the delegatecall-proxy pattern, the proxy address must be passed as the `--storage-address`. Otherwise, it is not required.
```shell
slither-read-storage 0xa2327a938Febf5FEC13baCFb16Ae10EcBc4cbDCF --variable-name balances --key 0xab5801a7d398351b8be11c439e05c5b3259aec9b --storage-address 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 --rpc-url $RPC_URL --value
```
## Troubleshooting/FAQ
- If the storage slots or values of a contract verified Etherscan are wrong, try passing `--contract $CONTRACT_NAME` explicitly. Otherwise, the storage may be retrieved from storage slots based off an unrelated contract (Etherscan includes these). (Also, make sure that the RPC is for the correct network.)
- If Etherscan fails to return a source code, try passing `--etherscan-apikey $API_KEY` to avoid hitting a rate-limit.
- How do I use this tool on other chains?
If an EVM chain has an Etherscan-like platform the crytic-compile supports, this tool supports it by making the following modifications.
Take Avalanche, for instance:
```shell
slither-read-storage avax:0x0000000000000000000000000000000000000000 --layout --value --rpc-url $AVAX_RPC_URL
```
## Limitations
- Requires source code.
- Only works on Solidity contracts.
- Cannot find variables with unstructured storage.
- Does not support all data types (please open an issue or PR).
- Mappings cannot be completely enumerated since all keys used historically are not immediately available.

@ -0,0 +1 @@
from .read_storage import SlitherReadStorage

@ -0,0 +1,145 @@
"""
Tool to read on-chain storage from EVM
"""
import json
import argparse
from crytic_compile import cryticparser
from slither import Slither
from slither.tools.read_storage.read_storage import SlitherReadStorage
def parse_args() -> argparse.Namespace:
"""Parse the underlying arguments for the program.
Returns:
The arguments for the program.
"""
parser = argparse.ArgumentParser(
description="Read a variable's value from storage for a deployed contract",
usage=(
"\nTo retrieve a single variable's value:\n"
+ "\tslither-read-storage $TARGET address --variable-name $NAME\n"
+ "To retrieve a contract's storage layout:\n"
+ "\tslither-read-storage $TARGET address --contract-name $NAME --layout\n"
+ "To retrieve a contract's storage layout and values:\n"
+ "\tslither-read-storage $TARGET address --contract-name $NAME --layout --values\n"
+ "TARGET can be a contract address or project directory"
),
)
parser.add_argument(
"contract_source",
help="The deployed contract address if verified on etherscan. Prepend project directory for unverified contracts.",
nargs="+",
)
parser.add_argument(
"--variable-name",
help="The name of the variable whose value will be returned.",
default=None,
)
parser.add_argument("--rpc-url", help="An endpoint for web3 requests.")
parser.add_argument(
"--key",
help="The key/ index whose value will be returned from a mapping or array.",
default=None,
)
parser.add_argument(
"--deep-key",
help="The key/ index whose value will be returned from a deep mapping or multidimensional array.",
default=None,
)
parser.add_argument(
"--struct-var",
help="The name of the variable whose value will be returned from a struct.",
default=None,
)
parser.add_argument(
"--storage-address",
help="The address of the storage contract (if a proxy pattern is used).",
default=None,
)
parser.add_argument(
"--contract-name",
help="The name of the logic contract.",
default=None,
)
parser.add_argument(
"--layout",
action="store_true",
help="Toggle used to write a JSON file with the entire storage layout.",
)
parser.add_argument(
"--value",
action="store_true",
help="Toggle used to include values in output.",
)
parser.add_argument("--max-depth", help="Max depth to search in data structure.", default=20)
cryticparser.init(parser)
return parser.parse_args()
def main() -> None:
args = parse_args()
if len(args.contract_source) == 2:
# Source code is file.sol or project directory
source_code, target = args.contract_source
slither = Slither(source_code, **vars(args))
else:
# Source code is published and retrieved via etherscan
target = args.contract_source[0]
slither = Slither(target, **vars(args))
if args.contract_name:
contracts = slither.get_contract_from_name(args.contract_name)
else:
contracts = slither.contracts
srs = SlitherReadStorage(contracts, args.max_depth)
if args.rpc_url:
# Remove target prefix e.g. rinkeby:0x0 -> 0x0.
address = target[target.find(":") + 1 :]
# Default to implementation address unless a storage address is given.
if not args.storage_address:
args.storage_address = address
srs.storage_address = args.storage_address
srs.rpc = args.rpc_url
if args.layout:
srs.get_all_storage_variables()
srs.get_storage_layout()
else:
assert args.variable_name
# Use a lambda func to only return variables that have same name as target.
# x is a tuple (`Contract`, `StateVariable`).
srs.get_all_storage_variables(lambda x: bool(x[1].name == args.variable_name))
srs.get_target_variables(**vars(args))
# To retrieve slot values an rpc url is required.
if args.value:
assert args.rpc_url
srs.get_slot_values()
# Only write file if storage layout is used.
if len(srs.slot_info) > 1:
with open("storage_layout.json", "w", encoding="utf-8") as file:
json.dump(srs.slot_info, file, indent=4)
if __name__ == "__main__":
main()

@ -0,0 +1,551 @@
import sys
import logging
from math import floor
from typing import Callable, Optional, Tuple, Union, List, Dict
try:
from typing import TypedDict
except ImportError:
# < Python 3.8
from typing_extensions import TypedDict
try:
from web3 import Web3
from eth_typing.evm import ChecksumAddress
from eth_abi import decode_single, encode_abi
from eth_utils import keccak
from hexbytes import HexBytes
from .utils import (
is_elementary,
is_array,
is_mapping,
is_struct,
is_user_defined_type,
get_offset_value,
get_storage_data,
coerce_type,
)
except ImportError:
print("ERROR: in order to use slither-read-storage, you need to install web3")
print("$ pip3 install web3 --user\n")
sys.exit(-1)
from slither.core.solidity_types.type import Type
from slither.core.solidity_types import ArrayType
from slither.core.declarations import Contract, StructureContract
from slither.core.variables.state_variable import StateVariable
from slither.core.variables.structure_variable import StructureVariable
logging.basicConfig()
logger = logging.getLogger("Slither-read-storage")
logger.setLevel(logging.INFO)
class SlotInfo(TypedDict):
type_string: str
slot: int
size: int
offset: int
value: Optional[Union[int, bool, str, ChecksumAddress, hex]]
elems: Optional[TypedDict] # same types as SlotInfo
class SlitherReadStorageException(Exception):
pass
# pylint: disable=too-many-instance-attributes
class SlitherReadStorage:
def __init__(self, contracts, max_depth):
self._contracts: List[Contract] = contracts
self._max_depth: int = max_depth
self._log: str = ""
self._slot_info: SlotInfo = {}
self._target_variables = []
self._web3: Optional[Web3] = None
self._checksum_address: Optional[ChecksumAddress] = None
self.storage_address: Optional[str] = None
self.rpc: Optional[str] = None
@property
def contracts(self) -> List[Contract]:
return self._contracts
@property
def max_depth(self) -> int:
return int(self._max_depth)
@property
def log(self) -> str:
return self._log
@log.setter
def log(self, log) -> str:
self._log = log
@property
def web3(self) -> Web3:
if not self._web3:
self._web3 = Web3(Web3.HTTPProvider(self.rpc))
return self._web3
@property
def checksum_address(self) -> ChecksumAddress:
if not self._checksum_address:
self._checksum_address = self.web3.toChecksumAddress(self.storage_address)
return self._checksum_address
@property
def target_variables(self) -> List[Tuple[Contract, StateVariable]]:
"""Storage variables (not constant or immutable) and their associated contract."""
return self._target_variables
@property
def slot_info(self) -> SlotInfo:
"""Contains the location, type, size, offset, and value of contract slots."""
return self._slot_info
def get_storage_layout(self) -> None:
"""Retrieves the storage layout of entire contract."""
tmp = {}
for contract, var in self.target_variables:
type_ = var.type
info = self.get_storage_slot(var, contract)
tmp[var.name] = info
if is_user_defined_type(type_) and is_struct(type_.type):
tmp[var.name]["elems"] = self._all_struct_slots(var, contract)
continue
if is_array(type_):
elems = self._all_array_slots(var, contract, type_, info["slot"])
tmp[var.name]["elems"] = elems
self._slot_info = tmp
def get_storage_slot(
self,
target_variable: StateVariable,
contract: Contract,
**kwargs,
) -> Union[SlotInfo, None]:
"""Finds the storage slot of a variable in a given contract.
Args:
target_variable (`StateVariable`): The variable to retrieve the slot for.
contracts (`Contract`): The contract that contains the given state variable.
**kwargs:
key (str): Key of a mapping or index position if an array.
deep_key (str): Key of a mapping embedded within another mapping or secondary index if array.
struct_var (str): Structure variable name.
Returns:
(`SlotInfo`) | None : A dictionary of the slot information.
"""
key = kwargs.get("key", None)
deep_key = kwargs.get("deep_key", None)
struct_var = kwargs.get("struct_var", None)
info = ""
var_log_name = target_variable.name
try:
int_slot, size, offset, type_to = self.get_variable_info(contract, target_variable)
except KeyError:
# Only the child contract of a parent contract will show up in the storage layout when inheritance is used
logger.info(
f"\nContract {contract} not found in storage layout. It is possibly a parent contract\n"
)
return None
slot = int.to_bytes(int_slot, 32, byteorder="big")
if is_elementary(target_variable.type):
type_to = target_variable.type.name
elif is_array(target_variable.type) and key:
info, type_to, slot, size, offset = self._find_array_slot(
target_variable, slot, key, deep_key=deep_key, struct_var=struct_var
)
self.log += info
elif is_user_defined_type(target_variable.type) and struct_var:
var_log_name = struct_var
elems = target_variable.type.type.elems_ordered
info, type_to, slot, size, offset = self._find_struct_var_slot(elems, slot, struct_var)
self.log += info
elif is_mapping(target_variable.type) and key:
info, type_to, slot, size, offset = self._find_mapping_slot(
target_variable, slot, key, struct_var=struct_var, deep_key=deep_key
)
self.log += info
int_slot = int.from_bytes(slot, byteorder="big")
self.log += f"\nName: {var_log_name}\nType: {type_to}\nSlot: {int_slot}\n"
logger.info(self.log)
self.log = ""
return {
"type_string": type_to,
"slot": int_slot,
"size": size,
"offset": offset,
}
def get_target_variables(self, **kwargs) -> None:
"""
Retrieves every instance of a given variable in a list of contracts.
Should be called after setting `target_variables` with `get_all_storage_variables()`.
**kwargs:
key (str): Key of a mapping or index position if an array.
deep_key (str): Key of a mapping embedded within another mapping or secondary index if array.
struct_var (str): Structure variable name.
"""
for contract, var in self.target_variables:
self._slot_info[f"{contract.name}.{var.name}"] = self.get_storage_slot(
var, contract, **kwargs
)
def get_slot_values(self) -> SlotInfo:
"""
Fetches the slot values and inserts them in slot info dictionary.
Returns:
(`SlotInfo`): The dictionary of slot info.
"""
stack = list(self.slot_info.items())
while stack:
_, v = stack.pop()
if isinstance(v, dict):
stack.extend(v.items())
if "slot" in v:
hex_bytes = get_storage_data(self.web3, self.checksum_address, v["slot"])
v["value"] = self.convert_value_to_type(
hex_bytes, v["size"], v["offset"], v["type_string"]
)
logger.info(f"\nValue: {v['value']}\n")
return self.slot_info
def get_all_storage_variables(self, func: Callable = None) -> None:
"""Fetches all storage variables from a list of contracts.
kwargs:
func (Callable, optional): A criteria to filter functions e.g. name.
"""
for contract in self.contracts:
self._target_variables.extend(
filter(
func,
[
(contract, var)
for var in contract.variables
if not var.is_constant and not var.is_immutable
],
)
)
@staticmethod
def _find_struct_var_slot(
elems: List[StructureVariable], slot: bytes, struct_var: str
) -> Tuple[str, str, bytes, int, int]:
"""Finds the slot of a structure variable.
Args:
elems (List[StructureVariable]): Ordered list of structure variables.
slot (bytes): The slot of the struct to begin searching at.
struct_var (str): The target structure variable.
Returns:
info (str): Info about the target variable to log.
type_to (str): The type of the target variable.
slot (bytes): The storage location of the target variable.
size (int): The size (in bits) of the target variable.
offset (int): The size of other variables that share the same slot.
"""
slot = int.from_bytes(slot, "big")
offset = 0
for var in elems:
size = var.type.size
if offset >= 256:
slot += 1
offset = 0
if struct_var == var.name:
type_to = var.type.name
break # found struct var
offset += size
slot = int.to_bytes(slot, 32, byteorder="big")
info = f"\nStruct Variable: {struct_var}"
return info, type_to, slot, size, offset
# pylint: disable=too-many-branches
@staticmethod
def _find_array_slot(
target_variable: StateVariable,
slot: bytes,
key: int,
deep_key: int = None,
struct_var: str = None,
) -> Tuple[str, str, bytes]:
"""Finds the slot of array's index.
Args:
target_variable (`StateVariable`): The array that contains the target variable.
slot (bytes): The starting slot of the array.
key (int): The target variable's index position.
deep_key (int, optional): Secondary index if nested array.
struct_var (str, optional): Structure variable name.
Returns:
info (str): Info about the target variable to log.
type_to (str): The type of the target variable.
slot (bytes): The storage location of the target variable.
"""
info = f"\nKey: {key}"
offset = 0
size = 256
if is_array(
target_variable.type.type
): # multidimensional array uint[i][], , uint[][i], or uint[][]
size = target_variable.type.type.type.size
type_to = target_variable.type.type.type.name
if target_variable.type.is_fixed_array: # uint[][i]
slot_int = int.from_bytes(slot, "big") + int(key)
else:
slot = keccak(slot)
key = int(key)
if target_variable.type.type.is_fixed_array: # arr[i][]
key *= int(str(target_variable.type.type.length))
slot_int = int.from_bytes(slot, "big") + key
if not deep_key:
return info, type_to, int.to_bytes(slot_int, 32, "big"), size, offset
info += f"\nDeep Key: {deep_key}"
if target_variable.type.type.is_dynamic_array: # uint[][]
# keccak256(keccak256(slot) + index) + floor(j / floor(256 / size))
slot = keccak(int.to_bytes(slot_int, 32, "big"))
slot_int = int.from_bytes(slot, "big")
# keccak256(slot) + index + floor(j / floor(256 / size))
slot_int += floor(int(deep_key) / floor(256 / size)) # uint[i][]
elif target_variable.type.is_fixed_array:
slot_int = int.from_bytes(slot, "big") + int(key)
if is_user_defined_type(target_variable.type.type): # struct[i]
type_to = target_variable.type.type.type.name
if not struct_var:
return info, type_to, int.to_bytes(slot_int, 32, "big"), size, offset
elems = target_variable.type.type.type.elems_ordered
slot = int.to_bytes(slot_int, 32, byteorder="big")
info_tmp, type_to, slot, size, offset = SlitherReadStorage._find_struct_var_slot(
elems, slot, struct_var
)
info += info_tmp
else:
type_to = target_variable.type.type.name
size = target_variable.type.type.size # bits
elif is_user_defined_type(target_variable.type.type): # struct[]
slot = keccak(slot)
slot_int = int.from_bytes(slot, "big") + int(key)
type_to = target_variable.type.type.type.name
if not struct_var:
return info, type_to, int.to_bytes(slot_int, 32, "big"), size, offset
elems = target_variable.type.type.type.elems_ordered
slot = int.to_bytes(slot_int, 32, byteorder="big")
info_tmp, type_to, slot, size, offset = SlitherReadStorage._find_struct_var_slot(
elems, slot, struct_var
)
info += info_tmp
else:
slot = keccak(slot)
slot_int = int.from_bytes(slot, "big") + int(key)
type_to = target_variable.type.type.name
size = target_variable.type.type.size # bits
slot = int.to_bytes(slot_int, 32, byteorder="big")
return info, type_to, slot, size, offset
@staticmethod
def _find_mapping_slot(
target_variable: StateVariable,
slot: bytes,
key: Union[int, str],
deep_key: Union[int, str] = None,
struct_var: str = None,
) -> Tuple[str, str, bytes, int, int]:
"""Finds the data slot of a target variable within a mapping.
target_variable (`StateVariable`): The mapping that contains the target variable.
slot (bytes): The starting slot of the mapping.
key (Union[int, str]): The key the variable is stored at.
deep_key (int, optional): Key of a mapping embedded within another mapping.
struct_var (str, optional): Structure variable name.
:returns:
log (str): Info about the target variable to log.
type_to (bytes): The type of the target variable.
slot (bytes): The storage location of the target variable.
size (int): The size (in bits) of the target variable.
offset (int): The size of other variables that share the same slot.
"""
info = ""
offset = 0
if key:
info += f"\nKey: {key}"
if deep_key:
info += f"\nDeep Key: {deep_key}"
key_type = target_variable.type.type_from.name
assert key
if "int" in key_type: # without this eth_utils encoding fails
key = int(key)
key = coerce_type(key_type, key)
slot = keccak(encode_abi([key_type, "uint256"], [key, decode_single("uint256", slot)]))
if is_user_defined_type(target_variable.type.type_to) and is_struct(
target_variable.type.type_to.type
): # mapping(elem => struct)
assert struct_var
elems = target_variable.type.type_to.type.elems_ordered
info_tmp, type_to, slot, size, offset = SlitherReadStorage._find_struct_var_slot(
elems, slot, struct_var
)
info += info_tmp
elif is_mapping(target_variable.type.type_to): # mapping(elem => mapping(elem => ???))
assert deep_key
key_type = target_variable.type.type_to.type_from.name
if "int" in key_type: # without this eth_utils encoding fails
deep_key = int(deep_key)
# If deep map, will be keccak256(abi.encode(key1, keccak256(abi.encode(key0, uint(slot)))))
slot = keccak(encode_abi([key_type, "bytes32"], [deep_key, slot]))
# mapping(elem => mapping(elem => elem))
type_to = target_variable.type.type_to.type_to.type
byte_size, _ = target_variable.type.type_to.type_to.storage_size
size = byte_size * 8 # bits
offset = 0
if is_user_defined_type(target_variable.type.type_to.type_to) and is_struct(
target_variable.type.type_to.type_to.type
): # mapping(elem => mapping(elem => struct))
assert struct_var
elems = target_variable.type.type_to.type_to.type.elems_ordered
# If map struct, will be bytes32(uint256(keccak256(abi.encode(key1, keccak256(abi.encode(key0, uint(slot)))))) + structFieldDepth);
info_tmp, type_to, slot, size, offset = SlitherReadStorage._find_struct_var_slot(
elems, slot, struct_var
)
info += info_tmp
# TODO: suppory mapping with dynamic arrays
else: # mapping(elem => elem)
type_to = target_variable.type.type_to.name # the value's elementary type
byte_size, _ = target_variable.type.type_to.storage_size
size = byte_size * 8 # bits
return info, type_to, slot, size, offset
@staticmethod
def get_variable_info(
contract: Contract, target_variable: StateVariable
) -> Tuple[int, int, int, str]:
"""Return slot, size, offset, and type."""
type_to = str(target_variable.type)
byte_size, _ = target_variable.type.storage_size
size = byte_size * 8 # bits
(int_slot, offset) = contract.compilation_unit.storage_layout_of(contract, target_variable)
offset *= 8 # bits
logger.info(
f"\nContract '{contract.name}'\n{target_variable.canonical_name} with type {target_variable.type} is located at slot: {int_slot}\n"
)
return int_slot, size, offset, type_to
@staticmethod
def convert_value_to_type(
hex_bytes: HexBytes, size: int, offset: int, type_to: str
) -> Union[int, bool, str, ChecksumAddress, hex]:
"""Convert slot data to type representation."""
# Account for storage packing
offset_hex_bytes = get_offset_value(hex_bytes, offset, size)
try:
value = coerce_type(type_to, offset_hex_bytes)
except ValueError:
return coerce_type("int", offset_hex_bytes)
return value
def _all_struct_slots(
self, var: Union[StructureVariable, StructureContract], contract: Contract, key=None
) -> Dict[str, SlotInfo]:
"""Retrieves all members of a struct."""
if isinstance(var.type.type, StructureContract):
struct_elems = var.type.type.elems_ordered
else:
struct_elems = var.type.type.type.elems_ordered
data = {}
for elem in struct_elems:
info = self.get_storage_slot(
var,
contract,
key=key,
struct_var=elem.name,
)
data[elem.name] = info
return data
def _all_array_slots(
self, var: ArrayType, contract: Contract, type_: Type, slot: int
) -> Dict[int, SlotInfo]:
"""Retrieves all members of an array."""
array_length = self._get_array_length(type_, slot)
elems = {}
if is_user_defined_type(type_.type):
for i in range(min(array_length, self.max_depth)):
elems[i] = self._all_struct_slots(var, contract, key=str(i))
continue
else:
for i in range(min(array_length, self.max_depth)):
info = self.get_storage_slot(
var,
contract,
key=str(i),
)
elems[i] = info
if is_array(type_.type): # multidimensional array
array_length = self._get_array_length(type_.type, info["slot"])
elems[i]["elems"] = {}
for j in range(min(array_length, self.max_depth)):
info = self.get_storage_slot(
var,
contract,
key=str(i),
deep_key=str(j),
)
elems[i]["elems"][j] = info
return elems
def _get_array_length(self, type_: Type, slot: int = None) -> int:
"""Gets the length of dynamic and fixed arrays.
Args:
type_ (`Type`): The array type.
slot (int, optional): Slot a dynamic array's length is stored at.
Returns:
(int): The length of the array.
"""
val = 0
if self.rpc:
# The length of dynamic arrays is stored at the starting slot.
# Convert from hexadecimal to decimal.
val = int(get_storage_data(self.web3, self.checksum_address, slot).hex(), 16)
if is_array(type_):
if type_.is_fixed_array:
val = int(str(type_.length))
return val

@ -0,0 +1,11 @@
from .utils import (
is_elementary,
is_array,
is_enum,
is_mapping,
is_struct,
is_user_defined_type,
get_offset_value,
get_storage_data,
coerce_type,
)

@ -0,0 +1,100 @@
from typing import Union
from hexbytes import HexBytes
from eth_typing.evm import ChecksumAddress
from eth_utils import to_int, to_text, to_checksum_address
from slither.core.declarations import Structure, Enum
from slither.core.solidity_types import ArrayType, MappingType, UserDefinedType, ElementaryType
from slither.core.variables.state_variable import StateVariable
def is_elementary(variable: StateVariable) -> bool:
"""Returns whether variable is an elementary type."""
return isinstance(variable, ElementaryType)
def is_array(variable: StateVariable) -> bool:
"""Returns whether variable is an array."""
return isinstance(variable, ArrayType)
def is_mapping(variable: StateVariable) -> bool:
"""Returns whether variable is a mapping."""
return isinstance(variable, MappingType)
def is_struct(variable: StateVariable) -> bool:
"""Returns whether variable is a struct."""
return isinstance(variable, Structure)
def is_enum(variable: StateVariable) -> bool:
"""Returns whether variable is an enum."""
return isinstance(variable, Enum)
def is_user_defined_type(variable: StateVariable) -> bool:
"""Returns whether variable is a struct."""
return isinstance(variable, UserDefinedType)
def get_offset_value(hex_bytes: HexBytes, offset: int, size: int) -> bytes:
"""
Trims slot data to only contain the target variable's.
Args:
hex_bytes (HexBytes): String representation of type
offset (int): The size (in bits) of other variables that share the same slot.
size (int): The size (in bits) of the target variable.
Returns:
(bytes): The target variable's trimmed data.
"""
size = int(size / 8)
offset = int(offset / 8)
if offset == 0:
value = hex_bytes[-size:]
else:
start = size + offset
value = hex_bytes[-start:-offset]
return value
def coerce_type(solidity_type: str, value: bytes) -> Union[int, bool, str, ChecksumAddress, hex]:
"""
Converts input to the indicated type.
Args:
solidity_type (str): String representation of type.
value (bytes): The value to be converted.
Returns:
(Union[int, bool, str, ChecksumAddress, hex]): The type representation of the value.
"""
if "int" in solidity_type:
converted_value = to_int(value)
elif "bool" in solidity_type:
converted_value = bool(to_int(value))
elif "string" in solidity_type:
# length * 2 is stored in lower end bits
# TODO handle bytes and strings greater than 32 bytes
length = int(int.from_bytes(value[-2:], "big") / 2)
converted_value = to_text(value[:length])
elif "address" in solidity_type:
converted_value = to_checksum_address(value)
else:
converted_value = value.hex()
return converted_value
def get_storage_data(web3, checksum_address: ChecksumAddress, slot: bytes) -> HexBytes:
"""
Retrieves the storage data from the blockchain at target address and slot.
Args:
web3: Web3 instance provider.
checksum_address (ChecksumAddress): The address to query.
slot (bytes): The slot to retrieve data from.
Returns:
(HexBytes): The slot's storage data.
"""
return bytes(web3.eth.get_storage_at(checksum_address, slot)).rjust(
32, bytes(1)
) # pad to 32 bytes

@ -15,7 +15,7 @@ class MultipleInitTarget(Exception):
pass pass
def _has_initiliaze_modifier(function: Function): def _has_initialize_modifier(function: Function):
if not function.modifiers: if not function.modifiers:
return False return False
return any((m.name == "initializer") for m in function.modifiers) return any((m.name == "initializer") for m in function.modifiers)
@ -25,7 +25,7 @@ def _get_initialize_functions(contract):
return [ return [
f f
for f in contract.functions for f in contract.functions
if (f.name == "initialize" or _has_initiliaze_modifier(f)) and f.is_implemented if (f.name == "initialize" or _has_initialize_modifier(f)) and f.is_implemented
] ]
@ -313,7 +313,7 @@ contract DerivedDerived is Derived{
} }
``` ```
`Base.initialize(uint)` is called two times in `DerivedDerived.initiliaze` execution, leading to a potential corruption. `Base.initialize(uint)` is called two times in `DerivedDerived.initialize` execution, leading to a potential corruption.
""" """
# endregion wiki_exploit_scenario # endregion wiki_exploit_scenario

@ -1,5 +1,6 @@
from functools import partial from functools import partial
import platform import platform
import sys
class Colors: # pylint: disable=too-few-public-methods class Colors: # pylint: disable=too-few-public-methods
@ -73,7 +74,7 @@ def set_colorization_enabled(enabled: bool):
if enabled and platform.system() == "Windows": if enabled and platform.system() == "Windows":
Colors.COLORIZATION_ENABLED = enable_windows_virtual_terminal_sequences() Colors.COLORIZATION_ENABLED = enable_windows_virtual_terminal_sequences()
else: else:
# This is not windows so we can enable color immediately. # This is not windows, or colorization is being disabled, so we can adjust the state immediately.
Colors.COLORIZATION_ENABLED = enabled Colors.COLORIZATION_ENABLED = enabled
@ -83,6 +84,5 @@ red = partial(colorize, Colors.RED)
blue = partial(colorize, Colors.BLUE) blue = partial(colorize, Colors.BLUE)
magenta = partial(colorize, Colors.MAGENTA) magenta = partial(colorize, Colors.MAGENTA)
# We enable colorization by default (this call is important as it will enable color mode on Windows by default), # We enable colorization by default if the output is a tty
# regardless of whether Slither is interacted with from CLI or another script. set_colorization_enabled(sys.stdout.isatty())
set_colorization_enabled(True)

@ -340,6 +340,53 @@ ERC2612 = [
ERC2612_signatures = erc_to_signatures(ERC2612) ERC2612_signatures = erc_to_signatures(ERC2612)
# Review
# https://eips.ethereum.org/EIPS/eip-1363
# Must have ERC20 and ERC165
ERC1363_EVENTS = []
ERC1363 = (
[
ERC("transferAndCall", ["address", "uint256"], "bool", False, True, []),
ERC("transferAndCall", ["address", "uint256", "bytes"], "bool", False, True, []),
ERC("transferFromAndCall", ["address", "address", "uint256"], "bool", False, True, []),
ERC(
"transferFromAndCall",
["address", "address", "uint256", "bytes"],
"bool",
False,
True,
[],
),
ERC("approveAndCall", ["address", "uint256"], "bool", False, True, []),
ERC("approveAndCall", ["address", "uint256", "bytes"], "bool", False, True, []),
]
+ ERC20
+ ERC165
)
ERC1363_signatures = erc_to_signatures(ERC1363)
# Review
# https://eips.ethereum.org/EIPS/eip-4524
# Must have ERC20 and ERC165
ERC4524_EVENTS = []
ERC4524 = (
[
ERC("safeTransfer", ["address", "uint256"], "bool", False, True, []),
ERC("safeTransfer", ["address", "uint256", "bytes"], "bool", False, True, []),
ERC("safeTransferFrom", ["address", "address", "uint256"], "bool", False, True, []),
ERC(
"safeTransferFrom", ["address", "address", "uint256", "bytes"], "bool", False, True, []
),
]
+ ERC20
+ ERC165
)
ERC4524_signatures = erc_to_signatures(ERC4524)
# Final # Final
# https://eips.ethereum.org/EIPS/eip-4626 # https://eips.ethereum.org/EIPS/eip-4626
# Must have ERC20 # Must have ERC20
@ -405,5 +452,7 @@ ERCS = {
"ERC777": (ERC777, ERC777_EVENTS), "ERC777": (ERC777, ERC777_EVENTS),
"ERC1155": (ERC1155, ERC1155_EVENTS), "ERC1155": (ERC1155, ERC1155_EVENTS),
"ERC2612": (ERC2612, ERC2612_EVENTS), "ERC2612": (ERC2612, ERC2612_EVENTS),
"ERC1363": (ERC1363, ERC1363_EVENTS),
"ERC4524": (ERC4524, ERC4524_EVENTS),
"ERC4626": (ERC4626, ERC4626_EVENTS), "ERC4626": (ERC4626, ERC4626_EVENTS),
} }

@ -3,12 +3,14 @@
as they should be immutable as they should be immutable
""" """
import copy import copy
from typing import Union, Callable
from slither.core.expressions import UnaryOperation from slither.core.expressions import UnaryOperation
from slither.core.expressions.assignment_operation import AssignmentOperation from slither.core.expressions.assignment_operation import AssignmentOperation
from slither.core.expressions.binary_operation import BinaryOperation from slither.core.expressions.binary_operation import BinaryOperation
from slither.core.expressions.call_expression import CallExpression from slither.core.expressions.call_expression import CallExpression
from slither.core.expressions.conditional_expression import ConditionalExpression from slither.core.expressions.conditional_expression import ConditionalExpression
from slither.core.expressions.elementary_type_name_expression import ElementaryTypeNameExpression
from slither.core.expressions.expression import Expression
from slither.core.expressions.identifier import Identifier from slither.core.expressions.identifier import Identifier
from slither.core.expressions.index_access import IndexAccess from slither.core.expressions.index_access import IndexAccess
from slither.core.expressions.literal import Literal from slither.core.expressions.literal import Literal
@ -20,7 +22,9 @@ from slither.core.expressions.type_conversion import TypeConversion
from slither.all_exceptions import SlitherException from slither.all_exceptions import SlitherException
# pylint: disable=protected-access # pylint: disable=protected-access
def f_expressions(e, x): def f_expressions(
e: AssignmentOperation, x: Union[Identifier, Literal, MemberAccess, IndexAccess]
) -> None:
e._expressions.append(x) e._expressions.append(x)
@ -37,7 +41,7 @@ def f_called(e, x):
class SplitTernaryExpression: class SplitTernaryExpression:
def __init__(self, expression): def __init__(self, expression: Union[AssignmentOperation, ConditionalExpression]) -> None:
if isinstance(expression, ConditionalExpression): if isinstance(expression, ConditionalExpression):
self.true_expression = copy.copy(expression.then_expression) self.true_expression = copy.copy(expression.then_expression)
@ -49,7 +53,13 @@ class SplitTernaryExpression:
self.condition = None self.condition = None
self.copy_expression(expression, self.true_expression, self.false_expression) self.copy_expression(expression, self.true_expression, self.false_expression)
def apply_copy(self, next_expr, true_expression, false_expression, f): def apply_copy(
self,
next_expr: Expression,
true_expression: Union[AssignmentOperation, MemberAccess],
false_expression: Union[AssignmentOperation, MemberAccess],
f: Callable,
) -> bool:
if isinstance(next_expr, ConditionalExpression): if isinstance(next_expr, ConditionalExpression):
f(true_expression, copy.copy(next_expr.then_expression)) f(true_expression, copy.copy(next_expr.then_expression))
@ -61,16 +71,20 @@ class SplitTernaryExpression:
f(false_expression, copy.copy(next_expr)) f(false_expression, copy.copy(next_expr))
return True return True
# pylint: disable=too-many-branches
def copy_expression( def copy_expression(
self, expression, true_expression, false_expression self, expression: Expression, true_expression: Expression, false_expression: Expression
): # pylint: disable=too-many-branches ) -> None:
if self.condition: if self.condition:
return return
if isinstance(expression, ConditionalExpression): if isinstance(expression, ConditionalExpression):
raise SlitherException("Nested ternary operator not handled") raise SlitherException("Nested ternary operator not handled")
if isinstance(expression, (Literal, Identifier, IndexAccess, NewArray, NewContract)): if isinstance(
expression,
(Literal, Identifier, IndexAccess, NewArray, NewContract, ElementaryTypeNameExpression),
):
return return
# case of lib # case of lib
@ -87,6 +101,12 @@ class SplitTernaryExpression:
false_expression._expressions = [] false_expression._expressions = []
for next_expr in expression.expressions: for next_expr in expression.expressions:
if isinstance(next_expr, IndexAccess):
# create an index access for each branch
if isinstance(next_expr.expression_right, ConditionalExpression):
next_expr = _handle_ternary_access(
next_expr, true_expression, false_expression
)
if self.apply_copy(next_expr, true_expression, false_expression, f_expressions): if self.apply_copy(next_expr, true_expression, false_expression, f_expressions):
# always on last arguments added # always on last arguments added
self.copy_expression( self.copy_expression(
@ -115,16 +135,7 @@ class SplitTernaryExpression:
false_expression.arguments[-1], false_expression.arguments[-1],
) )
elif isinstance(expression, TypeConversion): elif isinstance(expression, (TypeConversion, UnaryOperation)):
next_expr = expression.expression
if self.apply_copy(next_expr, true_expression, false_expression, f_expression):
self.copy_expression(
expression.expression,
true_expression.expression,
false_expression.expression,
)
elif isinstance(expression, UnaryOperation):
next_expr = expression.expression next_expr = expression.expression
if self.apply_copy(next_expr, true_expression, false_expression, f_expression): if self.apply_copy(next_expr, true_expression, false_expression, f_expression):
self.copy_expression( self.copy_expression(
@ -137,3 +148,35 @@ class SplitTernaryExpression:
raise SlitherException( raise SlitherException(
f"Ternary operation not handled {expression}({type(expression)})" f"Ternary operation not handled {expression}({type(expression)})"
) )
def _handle_ternary_access(
next_expr: IndexAccess,
true_expression: AssignmentOperation,
false_expression: AssignmentOperation,
):
"""
Conditional ternary accesses are split into two accesses, one true and one false
E.g. x[if cond ? 1 : 2] -> if cond { x[1] } else { x[2] }
"""
true_index_access = IndexAccess(
next_expr.expression_left,
next_expr.expression_right.then_expression,
next_expr.type,
)
false_index_access = IndexAccess(
next_expr.expression_left,
next_expr.expression_right.else_expression,
next_expr.type,
)
f_expressions(
true_expression,
true_index_access,
)
f_expressions(
false_expression,
false_index_access,
)
return next_expr.expression_right

@ -7,7 +7,7 @@ from slither.exceptions import SlitherError
def convert_string_to_int(val: Union[str, int]) -> int: def convert_string_to_int(val: Union[str, int]) -> int:
if isinstance(val, int): if isinstance(val, int):
return val return val
if val.startswith("0x") or val.startswith("0X"): if val.startswith(("0x", "0X")):
return int(val, 16) return int(val, 16)
if "e" in val or "E" in val: if "e" in val or "E" in val:

@ -160,7 +160,7 @@ def output_to_sarif(
], ],
} }
for detector in results["detectors"]: for detector in results.get("detectors", []):
_output_result_to_sarif(detector, detectors_classes, sarif) _output_result_to_sarif(detector, detectors_classes, sarif)
if filename == "-": if filename == "-":

@ -9,6 +9,7 @@ from slither.core.declarations import (
SolidityFunction, SolidityFunction,
Contract, Contract,
) )
from slither.core.declarations.enum import Enum
from slither.core.expressions import ( from slither.core.expressions import (
AssignmentOperationType, AssignmentOperationType,
UnaryOperationType, UnaryOperationType,
@ -403,18 +404,25 @@ class ExpressionToSlithIR(ExpressionVisitor):
assert len(expression.expression.arguments) == 1 assert len(expression.expression.arguments) == 1
val = TemporaryVariable(self._node) val = TemporaryVariable(self._node)
type_expression_found = expression.expression.arguments[0] type_expression_found = expression.expression.arguments[0]
assert isinstance(type_expression_found, ElementaryTypeNameExpression) if isinstance(type_expression_found, ElementaryTypeNameExpression):
type_found = type_expression_found.type type_found = type_expression_found.type
if expression.member_name == "min:": constant_type = type_found
else:
# type(enum).max/min
assert isinstance(type_expression_found, Identifier)
type_found = type_expression_found.value
assert isinstance(type_found, Enum)
constant_type = None
if expression.member_name == "min":
op = Assignment( op = Assignment(
val, val,
Constant(str(type_found.min), type_found), Constant(str(type_found.min), constant_type),
type_found, type_found,
) )
else: else:
op = Assignment( op = Assignment(
val, val,
Constant(str(type_found.max), type_found), Constant(str(type_found.max), constant_type),
type_found, type_found,
) )
self._result.append(op) self._result.append(op)

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

Loading…
Cancel
Save