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. 37
      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. 7
      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. 18
      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
uses: actions/checkout@v2
- name: Set up Python 3.6
- name: Set up Python 3.8
uses: actions/setup-python@v3
with:
python-version: 3.6
python-version: 3.8
- name: Install dependencies
run: |
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"
pip install ".[dev]"
solc-select install all
solc-select use 0.8.11

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

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

@ -26,21 +26,15 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v2
- name: Set up Python 3.6
- name: Set up Python 3.8
uses: actions/setup-python@v3
with:
python-version: 3.6
python-version: 3.8
- name: Install dependencies
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 use 0.7.3
- name: Test with pytest

@ -26,21 +26,15 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v2
- name: Set up Python 3.6
- name: Set up Python 3.8
uses: actions/setup-python@v3
with:
python-version: 3.6
python-version: 3.8
- name: Install dependencies
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 use 0.8.0
@ -52,3 +46,4 @@ jobs:
run: |
pytest tests/test_features.py
pytest tests/test_constant_folding_unary.py
pytest tests/slithir/test_ternary_expressions.py

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

@ -26,20 +26,14 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v2
- name: Set up Python 3.6
- name: Set up Python 3.8
uses: actions/setup-python@v3
with:
python-version: 3.6
python-version: 3.8
- name: Install dependencies
run: |
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"
pip install ".[dev]"
- name: Install solc
run: |

@ -11,18 +11,25 @@ on:
jobs:
audit:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up Python 3.10
uses: actions/setup-python@v3
uses: actions/checkout@v3
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install pip-audit
- name: Install Slither
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 pip-audit
- name: Run pip-audit
run: |
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
uses: actions/checkout@v2
- name: Set up Python 3.6
- name: Set up Python 3.8
uses: actions/setup-python@v3
with:
python-version: 3.6
python-version: 3.8
- name: Install dependencies
run: |
pip install .
pip install deepdiff numpy
mkdir -p .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.
- 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
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
- `deepdiff` installed (`pip install deepdiff`).
- `pycov` installed (`pip install pytest-cov`).
- [`solc-select`](https://github.com/crytic/solc-select) installed.
To run the unit tests, you need to clone this repository and run `pip install ".[dev]"`.
### Linters
@ -46,6 +61,7 @@ For each new detector, at least one regression tests must be present.
- Create a test in `tests`
- 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.
- 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.
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 src="https://github.com/trailofbits/slither"
@ -6,11 +6,12 @@ LABEL creator=trailofbits
LABEL dockerfile_maintenance=trailofbits
LABEL desc="Static Analyzer for Solidity"
RUN apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y git python3 python3-setuptools wget software-properties-common
RUN export DEBIAN_FRONTEND=noninteractive \
&& apt-get update \
&& 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 \
&& 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)
- [How to Install](#how-to-install)
- [Getting Help](#getting-help)
- [FAQ](#faq)
- [Publications](#publications)
## Features
@ -40,9 +41,12 @@ Run Slither on a single file:
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
@ -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
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
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
@ -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
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
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
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
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
- 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)
- `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)
- `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.
@ -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-check-erc`: [Check the ERC's conformance](https://github.com/crytic/slither/wiki/ERC-Conformance)
- `slither-format`: [Automatic patch generation](https://github.com/crytic/slither/wiki/Slither-format)
- `slither-read-storage`: [Read storage values from contracts](./slither/tools/read_storage/README.md)
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
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
@ -181,7 +189,7 @@ We recommend using a Python virtual environment, as detailed in the [Developer I
### Using Docker
Use the [`eth-security-toolbox`](https://github.com/trailofbits/eth-security-toolbox/) docker image. It includes all of our security tools and every major version of Solidity in a single image. `/home/share` will be mounted to `/share` in the container.
Use the [`eth-security-toolbox`](https://github.com/trailofbits/eth-security-toolbox/) docker image. It includes all of our security tools and every major version of Solidity in a single image. `/home/share` will be mounted to `/share` in the container.
```bash
docker pull trailofbits/eth-security-toolbox
@ -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.
## 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
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
### 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
[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)
[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/).

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

@ -7,7 +7,7 @@ setup(
author="Trail of Bits",
version="0.0",
packages=find_packages(),
python_requires=">=3.6",
python_requires=">=3.8",
install_requires=["slither-analyzer==0.1"],
entry_points={
"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")
if [ "$DIFF" != "" ]
then
echo "slither-check-upgradeability 1 failed"
echo "slither-check-kspec 1 failed"
cat test_1.txt
echo ""
cat "$DIR_TESTS/test_1.txt"

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

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

@ -8,15 +8,26 @@ setup(
description="Slither is a Solidity static analysis framework written in Python 3.",
url="https://github.com/crytic/slither",
author="Trail of Bits",
version="0.8.2",
version="0.8.3",
packages=find_packages(),
python_requires=">=3.6",
python_requires=">=3.8",
install_requires=[
"prettytable>=0.7.2",
"pysha3>=1.0.2",
# "crytic-compile>=0.2.2",
# "crytic-compile>=0.2.3",
"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"],
license="AGPL-3.0",
long_description=long_description,
@ -32,6 +43,7 @@ setup(
"slither-check-kspec = slither.tools.kspec_coverage.__main__:main",
"slither-prop = slither.tools.properties.__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_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_detector.add_argument(
@ -312,7 +315,7 @@ def parse_args(detector_classes, printer_classes): # pylint: disable=too-many-s
group_printer.add_argument(
"--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)}",
action="store",
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"],
)
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(
"--json",
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"],
)
group_misc.add_argument(
"--markdown-root",
type=check_and_sanitize_markdown_root,
help="URL for markdown generation",
action="store",
default="",
)
group_misc.add_argument(
"--disable-color",
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)
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(
"--wiki-detectors", help=argparse.SUPPRESS, action=OutputWiki, default=False
)
@ -648,7 +659,7 @@ def main_impl(all_detector_classes, all_printer_classes):
cp.enable()
# 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
json_results = {}

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

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

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

@ -21,6 +21,8 @@ from slither.utils.erc import (
ERC777_signatures,
ERC1155_signatures,
ERC2612_signatures,
ERC1363_signatures,
ERC4524_signatures,
ERC4626_signatures,
)
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._kind: Optional[str] = None
self._is_interface: bool = False
self._is_library: bool = False
self._signatures: 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):
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
###################################################################################
###################################################################################
@ -903,6 +914,7 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
("ERC721", self.is_erc721),
("ERC777", self.is_erc777),
("ERC2612", self.is_erc2612),
("ERC1363", self.is_erc1363),
("ERC4626", self.is_erc4626),
]
@ -998,6 +1010,26 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
full_names = self.functions_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
def is_token(self) -> bool:
"""

@ -9,6 +9,9 @@ class Enum(SourceMapping):
self._name = name
self._canonical_name = canonical_name
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
def canonical_name(self) -> str:
@ -22,5 +25,13 @@ class Enum(SourceMapping):
def values(self) -> List[str]:
return self._values
@property
def min(self) -> int:
return self._min
@property
def max(self) -> int:
return self._max
def __str__(self):
return self.name

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

@ -104,7 +104,7 @@ class SolidityVariable(Context):
# dev function, will be removed once the code is stable
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
def state_variable(self):

@ -4,6 +4,7 @@
import json
import logging
import os
import pathlib
import posixpath
import re
from typing import Optional, Dict, List, Set, Union
@ -218,8 +219,12 @@ class SlitherCore(Context):
for elem in r["elements"]
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

@ -39,9 +39,17 @@ class ArrayType(Type):
return self._length
@property
def lenght_value(self) -> Optional[Literal]:
def length_value(self) -> Optional[Literal]:
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
def storage_size(self) -> Tuple[int, bool]:
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["string"] = None
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["string"] = None
Min_Byte["byte"] = 0x10
Min_Byte["string"] = 0x0
Min_Byte["byte"] = 0x0
MaxValues = dict(dict(Max_Int, **Max_Uint), **Max_Byte)
MinValues = dict(dict(Min_Int, **Min_Uint), **Min_Byte)
@ -193,7 +193,7 @@ class ElementaryType(Type):
if t == "address":
return int(160)
if t.startswith("bytes") and t != "bytes":
return int(t[len("bytes") :])
return int(t[len("bytes") :]) * 8
return None
@property

@ -13,8 +13,9 @@ class TypeInformation(Type):
def __init__(self, c):
# pylint: disable=import-outside-toplevel
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__()
self._type = c

@ -6,7 +6,9 @@ from .variables.uninitialized_local_variables import UninitializedLocalVars
from .attributes.constant_pragma import ConstantPragma
from .attributes.incorrect_solc import IncorrectSolc
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.complex_function import ComplexFunction
@ -34,7 +36,7 @@ from .shadowing.builtin_symbols import BuiltinSymbolShadowing
from .operations.block_timestamp import Timestamp
from .statements.calls_in_loop import MultipleCallsInLoop
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.unindexed_event_parameters import UnindexedERC20EventParameters
from .statements.deprecated_calls import DeprecatedStandards

@ -13,7 +13,9 @@ class ShiftParameterMixup(AbstractDetector):
IMPACT = 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_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
# pylint: disable=anomalous-backslash-in-string
PATTERN = re.compile("(\^|>|>=|<|<=)?([ ]+)?(\d+)\.(\d+)\.(\d+)")
PATTERN = re.compile(r"(\^|>|>=|<|<=)?([ ]+)?(\d+)\.(\d+)\.(\d+)")
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
class ArbitrarySend(AbstractDetector):
ARGUMENT = "arbitrary-send"
class ArbitrarySendEth(AbstractDetector):
ARGUMENT = "arbitrary-send-eth"
HELP = "Functions that send Ether to arbitrary destinations"
IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.MEDIUM
@ -104,7 +104,7 @@ class ArbitrarySend(AbstractDetector):
# region wiki_exploit_scenario
WIKI_EXPLOIT_SCENARIO = """
```solidity
contract ArbitrarySend{
contract ArbitrarySendEth{
address destination;
function setDestination(){
destination = msg.sender;

@ -94,7 +94,7 @@ Solidity defines a [naming convention](https://solidity.readthedocs.io/en/v0.4.2
"private",
] and self.is_mixed_case_with_underscore(func.name):
continue
if func.name.startswith("echidna_") or func.name.startswith("crytic_"):
if func.name.startswith(("echidna_", "crytic_")):
continue
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.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.core.declarations import Contract
@ -22,7 +22,11 @@ def detect_call_in_loop(contract: Contract) -> List[Node]:
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:
return
# 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.detectors.abstract_detector import AbstractDetector, DetectorClassification
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(
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:
if node is None:
return
if node in visited:
return
# 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.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.slithir.operations import LowLevelCall, InternalCall
@ -15,8 +15,12 @@ def detect_delegatecall_in_loop(contract: Contract) -> List[Node]:
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:
if node is None:
return
if node in visited:
return
# 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.detectors.abstract_detector import AbstractDetector, DetectorClassification
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(
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:
if node is None:
return
if node in visited:
return
# shared visited

@ -2,9 +2,19 @@
Module detecting numbers with too many digits.
"""
import re
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
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):
"""
@ -58,7 +68,7 @@ Use:
if isinstance(read, Constant):
# read.value can return an int or a str. Convert it to str
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
ret.append(node)
return ret

@ -102,7 +102,7 @@ class UnprotectedUpgradeable(AbstractDetector):
info = (
[
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
+ [

@ -17,7 +17,9 @@ class SimilarVarsDetection(AbstractDetector):
IMPACT = DetectorClassification.INFORMATIONAL
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_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 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:
continue
# 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 function in contract.functions:
if function.is_implemented:
if function.is_implemented and function.entry_point:
uninitialized_storage_variables = [
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
# 3: version number
# 4: version number
PATTERN = re.compile("(\^|>|>=|<|<=)?([ ]+)?(\d+)\.(\d+)\.(\d+)")
PATTERN = re.compile(r"(\^|>|>=|<|<=)?([ ]+)?(\d+)\.(\d+)\.(\d+)")
def custom_format(slither, result):

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

@ -300,10 +300,10 @@ def _patch(compilation_unit: SlitherCompilationUnit, result, element, _target):
# group 2: beginning of the to type
# nested mapping are within the group 1
# RE_MAPPING = '[ ]*mapping[ ]*\([ ]*([\=\>\(\) a-zA-Z0-9\._\[\]]*)[ ]*=>[ ]*([a-zA-Z0-9\._\[\]]*)\)'
RE_MAPPING_FROM = b"([a-zA-Z0-9\._\[\]]*)"
RE_MAPPING_TO = b"([\=\>\(\) a-zA-Z0-9\._\[\]\ ]*)"
RE_MAPPING_FROM = rb"([a-zA-Z0-9\._\[\]]*)"
RE_MAPPING_TO = rb"([\=\>\(\) a-zA-Z0-9\._\[\]\ ]*)"
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"
table = MyPrettyTable(["Name", "Type", "Slot", "Offset"])
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)
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:
solc (str): solc binary location (default 'solc')
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')
filter_paths (list(str)): list of path to filter (default [])
triage_mode (bool): if true, switch to triage mode (default false)

@ -33,6 +33,7 @@ from slither.core.solidity_types.elementary_type import (
MaxValues,
)
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.state_variable import StateVariable
from slither.core.variables.variable import Variable
@ -399,6 +400,13 @@ def propagate_type_and_convert_call(result, node):
ins = new_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):
# In case of dupplicate arguments we overwrite the value
# 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)
# 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.
assert isinstance(node_function, FunctionContract)
if ir.destination.name == "this":
@ -817,12 +827,28 @@ def extract_tmp_call(ins: TmpCall, contract: Optional[Contract]): # pylint: dis
# lib L { event 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.set_expression(ins.expression)
eventcall.call_id = ins.call_id
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(
ins.ori.variable_left,
ins.ori.variable_right,
@ -1369,7 +1395,11 @@ def convert_type_library_call(ir: HighLevelCall, lib_contract: Contract):
if len(candidates) == 1:
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
func = lib_contract.get_state_variable_from_name(ir.function_name)
if func is None and candidates:
@ -1657,7 +1687,10 @@ def convert_constant_types(irs):
if isinstance(ir.rvalue, TupleVariable):
# TODO: fix missing Unpack conversion
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))
was_changed = True
if isinstance(ir, Binary):
@ -1676,6 +1709,7 @@ def convert_constant_types(irs):
# TODO: add POP instruction
break
types = [p.type for p in func.parameters]
assert len(types) == len(ir.arguments)
for idx, arg in enumerate(ir.arguments):
t = types[idx]
if isinstance(t, ElementaryType):

@ -157,7 +157,10 @@ class ContractSolc(CallerContextExpression):
if "contractKind" in attributes:
if attributes["contractKind"] == "interface":
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._contract.fullyImplemented = attributes["fullyImplemented"]

@ -445,7 +445,7 @@ def parse_expression(expression: Dict, caller_context: CallerContextExpression)
t = expression["attributes"]["type"]
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
if found:
value = value + "(" + found[0] + ")"
@ -455,7 +455,6 @@ def parse_expression(expression: Dict, caller_context: CallerContextExpression)
referenced_declaration = expression["referencedDeclaration"]
else:
referenced_declaration = None
var, was_created = find_variable(value, caller_context, referenced_declaration)
if was_created:
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 = []
# try:
# Resolve linearized base contracts.
missing_inheritance = False
missing_inheritance = None
for i in contract_parser.linearized_base_contracts[1:]:
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:
ancestors.append(self._contracts_by_id[i])
else:
missing_inheritance = True
missing_inheritance = i
# Resolve immediate base contracts
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:
fathers.append(self._contracts_by_id[i])
else:
missing_inheritance = True
missing_inheritance = i
# Resolve immediate base constructor calls
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:
father_constructors.append(self._contracts_by_id[i])
else:
missing_inheritance = True
missing_inheritance = i
contract_parser.underlying_contract.set_inheritance(
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(
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.delete_content()
@ -679,12 +686,12 @@ Please rename it, this name is reserved for Slither's internals"""
for func in contract.functions + contract.modifiers:
try:
func.generate_slithir_and_analyze()
except AttributeError:
except AttributeError as e:
# This can happens for example if there is a call to an interface
# And the interface is redefined due to contract's name reuse
# But the available version misses some functions
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()

@ -112,7 +112,7 @@ def _find_from_type_name( # pylint: disable=too-many-locals,too-many-branches,t
if not var_type:
if name.startswith("function "):
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,
)
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("):
# nested mapping declared with var
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:
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,
)
assert len(found) == 1
@ -272,8 +272,12 @@ def parse_type(
all_structuress = [c.structures for c in scope.contracts.values()]
all_structures = [item for sublist in all_structuress for item in sublist]
all_structures += structures_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()
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
# So things like .offset/.slot will return the variable
# Instaed of the actual offset/slot
if name.endswith("_slot") or name.endswith(".slot"):
if name.endswith(("_slot", ".slot")):
potential_name = name[:-5]
variable_found = _check_for_state_variable_name(root, potential_name)
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)
if var and var.is_storage:
return Identifier(var)
if name.endswith("_offset") or name.endswith(".offset"):
if name.endswith(("_offset", ".offset")):
potential_name = name[:-7]
variable_found = _check_for_state_variable_name(root, potential_name)
if variable_found:

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

@ -79,6 +79,12 @@ def parse_args():
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(
"--remove-assert", help="Remove call to assert().", action="store_true"
)
@ -111,6 +117,7 @@ def main():
compilation_unit,
external_to_public=args.convert_external,
remove_assert=args.remove_assert,
convert_library_to_internal=args.convert_library_to_internal,
private_to_internal=args.convert_private,
export_path=args.dir,
pragma_solidity=args.pragma_solidity,

@ -51,6 +51,7 @@ class Flattening:
compilation_unit: SlitherCompilationUnit,
external_to_public=False,
remove_assert=False,
convert_library_to_internal=False,
private_to_internal=False,
export_path: Optional[str] = None,
pragma_solidity: Optional[str] = None,
@ -61,6 +62,7 @@ class Flattening:
self._external_to_public = external_to_public
self._remove_assert = remove_assert
self._use_abi_encoder_v2 = False
self._convert_library_to_internal = convert_library_to_internal
self._private_to_internal = private_to_internal
self._pragma_solidity = pragma_solidity
@ -111,7 +113,7 @@ class Flattening:
to_patch = []
# 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:
# fallback must be external
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:
for variable in contract.state_variables_declared:
if variable.visibility == "private":
@ -186,6 +218,10 @@ class Flattening:
index = index - start
if patch_type == "public_to_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":
content = content[:index] + "internal" + content[index + len("private") :]
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
covered_functions: Set[Tuple[str, str]] = set()
BEHAVIOUR_PATTERN = re.compile("behaviour\s+(\S+)\s+of\s+(\S+)")
INTERFACE_PATTERN = re.compile("interface\s+([^\r\n]+)")
BEHAVIOUR_PATTERN = re.compile(r"behaviour\s+(\S+)\s+of\s+(\S+)")
INTERFACE_PATTERN = re.compile(r"interface\s+([^\r\n]+)")
# Read the file contents
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
def _has_initiliaze_modifier(function: Function):
def _has_initialize_modifier(function: Function):
if not function.modifiers:
return False
return any((m.name == "initializer") for m in function.modifiers)
@ -25,7 +25,7 @@ def _get_initialize_functions(contract):
return [
f
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

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

@ -340,6 +340,53 @@ 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
# https://eips.ethereum.org/EIPS/eip-4626
# Must have ERC20
@ -405,5 +452,7 @@ ERCS = {
"ERC777": (ERC777, ERC777_EVENTS),
"ERC1155": (ERC1155, ERC1155_EVENTS),
"ERC2612": (ERC2612, ERC2612_EVENTS),
"ERC1363": (ERC1363, ERC1363_EVENTS),
"ERC4524": (ERC4524, ERC4524_EVENTS),
"ERC4626": (ERC4626, ERC4626_EVENTS),
}

@ -3,12 +3,14 @@
as they should be immutable
"""
import copy
from typing import Union, Callable
from slither.core.expressions import UnaryOperation
from slither.core.expressions.assignment_operation import AssignmentOperation
from slither.core.expressions.binary_operation import BinaryOperation
from slither.core.expressions.call_expression import CallExpression
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.index_access import IndexAccess
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
# 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)
@ -37,7 +41,7 @@ def f_called(e, x):
class SplitTernaryExpression:
def __init__(self, expression):
def __init__(self, expression: Union[AssignmentOperation, ConditionalExpression]) -> None:
if isinstance(expression, ConditionalExpression):
self.true_expression = copy.copy(expression.then_expression)
@ -49,7 +53,13 @@ class SplitTernaryExpression:
self.condition = None
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):
f(true_expression, copy.copy(next_expr.then_expression))
@ -61,16 +71,20 @@ class SplitTernaryExpression:
f(false_expression, copy.copy(next_expr))
return True
# pylint: disable=too-many-branches
def copy_expression(
self, expression, true_expression, false_expression
): # pylint: disable=too-many-branches
self, expression: Expression, true_expression: Expression, false_expression: Expression
) -> None:
if self.condition:
return
if isinstance(expression, ConditionalExpression):
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
# case of lib
@ -87,6 +101,12 @@ class SplitTernaryExpression:
false_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):
# always on last arguments added
self.copy_expression(
@ -115,16 +135,7 @@ class SplitTernaryExpression:
false_expression.arguments[-1],
)
elif isinstance(expression, TypeConversion):
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):
elif isinstance(expression, (TypeConversion, UnaryOperation)):
next_expr = expression.expression
if self.apply_copy(next_expr, true_expression, false_expression, f_expression):
self.copy_expression(
@ -137,3 +148,35 @@ class SplitTernaryExpression:
raise SlitherException(
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:
if isinstance(val, int):
return val
if val.startswith("0x") or val.startswith("0X"):
if val.startswith(("0x", "0X")):
return int(val, 16)
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)
if filename == "-":

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

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

Loading…
Cancel
Save