Merge pull request #2174 from crytic/dev

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

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

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

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

@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v3
- uses: actions/setup-python@v4
@ -37,7 +37,7 @@ jobs:
- run: pip install -e ".[doc]"
- run: pdoc -o html/ slither '!slither.tools' #TODO fix import errors on pdoc run
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
uses: actions/upload-pages-artifact@v2
with:
# Upload the doc
path: './html/'

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

@ -9,8 +9,6 @@ defaults:
on:
pull_request:
branches: [master, dev]
paths:
- "**/*.py"
schedule:
# run CI every day even if no PRs/merges occur
@ -27,7 +25,7 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
# Full git history is needed to get a proper list of changed files within `super-linter`
fetch-depth: 0
@ -42,6 +40,10 @@ jobs:
mkdir -p .github/linters
cp pyproject.toml .github/linters
- name: Register yamllint problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/yamllint.json"
- name: Lint everything else
uses: super-linter/super-linter/slim@v4.9.2
if: always()
@ -55,7 +57,6 @@ jobs:
VALIDATE_PYTHON_PYLINT: false
VALIDATE_PYTHON_BLACK: false
VALIDATE_PYTHON_ISORT: false
# Always false
VALIDATE_JSON: false
VALIDATE_JAVASCRIPT_STANDARD: false
VALIDATE_PYTHON_FLAKE8: false

@ -0,0 +1,32 @@
{
"problemMatcher": [
{
"owner": "pylint-error",
"severity": "error",
"pattern": [
{
"regexp": "^(.+):(\\d+):(\\d+):\\s(([EF]\\d{4}):\\s.+)$",
"file": 1,
"line": 2,
"column": 3,
"message": 4,
"code": 5
}
]
},
{
"owner": "pylint-warning",
"severity": "warning",
"pattern": [
{
"regexp": "^(.+):(\\d+):(\\d+):\\s(([CRW]\\d{4}):\\s.+)$",
"file": 1,
"line": 2,
"column": 3,
"message": 4,
"code": 5
}
]
}
]
}

@ -0,0 +1,22 @@
{
"problemMatcher": [
{
"owner": "yamllint",
"pattern": [
{
"regexp": "^(.*\\.ya?ml)$",
"file": 1
},
{
"regexp": "^\\s{2}(\\d+):(\\d+)\\s+(error|warning)\\s+(.*?)\\s+\\((.*)\\)$",
"line": 1,
"column": 2,
"severity": 3,
"message": 4,
"code": 5,
"loop": true
}
]
}
]
}

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

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

@ -9,6 +9,8 @@ defaults:
on:
pull_request:
branches: [master, dev]
paths:
- "**/*.py"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@ -21,7 +23,7 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
# Full git history is needed to get a proper list of changed files within `super-linter`
fetch-depth: 0
@ -36,6 +38,10 @@ jobs:
mkdir -p .github/linters
cp pyproject.toml .github/linters
- name: Register pylint problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/pylint.json"
- name: Pylint
uses: super-linter/super-linter/slim@v4.9.2
if: always()

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

@ -96,8 +96,8 @@ For each new detector, at least one regression tests must be present.
#### Adding parsing tests
1. Create a test in `tests/e2e/solc_parsing/`
2. Run `python tests/e2e/solc_parsing/test_ast_parsing.py --compile`. This will compile the artifact in `tests/e2e/solc_parsing/compile`. Add the compiled artifact to git.
3. Update `ALL_TESTS` in `tests/e2e/solc_parsing/test_ast_parsing.py`.
2. Update `ALL_TESTS` in `tests/e2e/solc_parsing/test_ast_parsing.py`.
3. Run `python tests/e2e/solc_parsing/test_ast_parsing.py --compile`. This will compile the artifact in `tests/e2e/solc_parsing/compile`. Add the compiled artifact to git.
4. Run `python tests/e2e/solc_parsing/test_ast_parsing.py --generate`. This will generate the json artifacts in `tests/e2e/solc_parsing/expected_json`. Add the generated files to git.
5. Run `pytest tests/e2e/solc_parsing/test_ast_parsing.py` and check that everything worked.

@ -1,4 +1,4 @@
# [Slither, the Solidity source analyzer](https://crytic.github.io/slither/slither.html)
# [Slither, the smart contrat static analyzer](https://crytic.github.io/slither/slither.html)
<img src="https://raw.githubusercontent.com/crytic/slither/master/logo.png" alt="Slither Static Analysis Framework Logo" width="500" />
@ -12,7 +12,7 @@
> [![Slack Status](https://slack.empirehacking.nyc/badge.svg)](https://slack.empirehacking.nyc/)
> > <sub><i>- Discussions and Support </i></sub>
**Slither** is a Solidity static analysis framework written in Python3. It runs a suite of vulnerability detectors, prints visual information about contract details, and provides an API to easily write custom analyses. Slither enables developers to find vulnerabilities, enhance their code comprehension, and quickly prototype custom analyses.
**Slither** is a Solidity & Vyper static analysis framework written in Python3. It runs a suite of vulnerability detectors, prints visual information about contract details, and provides an API to easily write custom analyses. Slither enables developers to find vulnerabilities, enhance their code comprehension, and quickly prototype custom analyses.
* [Features](#features)
* [Usage](#usage)
@ -46,6 +46,7 @@
* Correctly parses 99.9% of all public Solidity code
* Average execution time of less than 1 second per contract
* Integrates with Github's code scanning in [CI](https://github.com/marketplace/actions/slither-action)
* Support for Vyper
## Usage
@ -129,72 +130,77 @@ Num | Detector | What it Detects | Impact | Confidence
19 | `controlled-array-length` | [Tainted array length assignment](https://github.com/crytic/slither/wiki/Detector-Documentation#array-length-assignment) | High | Medium
20 | `controlled-delegatecall` | [Controlled delegatecall destination](https://github.com/crytic/slither/wiki/Detector-Documentation#controlled-delegatecall) | High | Medium
21 | `delegatecall-loop` | [Payable functions using `delegatecall` inside a loop](https://github.com/crytic/slither/wiki/Detector-Documentation/#payable-functions-using-delegatecall-inside-a-loop) | High | Medium
22 | `msg-value-loop` | [msg.value inside a loop](https://github.com/crytic/slither/wiki/Detector-Documentation/#msgvalue-inside-a-loop) | High | Medium
23 | `reentrancy-eth` | [Reentrancy vulnerabilities (theft of ethers)](https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities) | High | Medium
24 | `storage-array` | [Signed storage integer array compiler bug](https://github.com/crytic/slither/wiki/Detector-Documentation#storage-signed-integer-array) | High | Medium
25 | `unchecked-transfer` | [Unchecked tokens transfer](https://github.com/crytic/slither/wiki/Detector-Documentation#unchecked-transfer) | High | Medium
26 | `weak-prng` | [Weak PRNG](https://github.com/crytic/slither/wiki/Detector-Documentation#weak-PRNG) | High | Medium
27 | `domain-separator-collision` | [Detects ERC20 tokens that have a function whose signature collides with EIP-2612's DOMAIN_SEPARATOR()](https://github.com/crytic/slither/wiki/Detector-Documentation#domain-separator-collision) | Medium | High
28 | `enum-conversion` | [Detect dangerous enum conversion](https://github.com/crytic/slither/wiki/Detector-Documentation#dangerous-enum-conversion) | Medium | High
29 | `erc20-interface` | [Incorrect ERC20 interfaces](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-erc20-interface) | Medium | High
30 | `erc721-interface` | [Incorrect ERC721 interfaces](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-erc721-interface) | Medium | High
31 | `incorrect-equality` | [Dangerous strict equalities](https://github.com/crytic/slither/wiki/Detector-Documentation#dangerous-strict-equalities) | Medium | High
32 | `locked-ether` | [Contracts that lock ether](https://github.com/crytic/slither/wiki/Detector-Documentation#contracts-that-lock-ether) | Medium | High
33 | `mapping-deletion` | [Deletion on mapping containing a structure](https://github.com/crytic/slither/wiki/Detector-Documentation#deletion-on-mapping-containing-a-structure) | Medium | High
34 | `shadowing-abstract` | [State variables shadowing from abstract contracts](https://github.com/crytic/slither/wiki/Detector-Documentation#state-variable-shadowing-from-abstract-contracts) | Medium | High
35 | `tautology` | [Tautology or contradiction](https://github.com/crytic/slither/wiki/Detector-Documentation#tautology-or-contradiction) | Medium | High
36 | `write-after-write` | [Unused write](https://github.com/crytic/slither/wiki/Detector-Documentation#write-after-write) | Medium | High
37 | `boolean-cst` | [Misuse of Boolean constant](https://github.com/crytic/slither/wiki/Detector-Documentation#misuse-of-a-boolean-constant) | Medium | Medium
38 | `constant-function-asm` | [Constant functions using assembly code](https://github.com/crytic/slither/wiki/Detector-Documentation#constant-functions-using-assembly-code) | Medium | Medium
39 | `constant-function-state` | [Constant functions changing the state](https://github.com/crytic/slither/wiki/Detector-Documentation#constant-functions-changing-the-state) | Medium | Medium
40 | `divide-before-multiply` | [Imprecise arithmetic operations order](https://github.com/crytic/slither/wiki/Detector-Documentation#divide-before-multiply) | Medium | Medium
41 | `reentrancy-no-eth` | [Reentrancy vulnerabilities (no theft of ethers)](https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-1) | Medium | Medium
42 | `reused-constructor` | [Reused base constructor](https://github.com/crytic/slither/wiki/Detector-Documentation#reused-base-constructors) | Medium | Medium
43 | `tx-origin` | [Dangerous usage of `tx.origin`](https://github.com/crytic/slither/wiki/Detector-Documentation#dangerous-usage-of-txorigin) | Medium | Medium
44 | `unchecked-lowlevel` | [Unchecked low-level calls](https://github.com/crytic/slither/wiki/Detector-Documentation#unchecked-low-level-calls) | Medium | Medium
45 | `unchecked-send` | [Unchecked send](https://github.com/crytic/slither/wiki/Detector-Documentation#unchecked-send) | Medium | Medium
46 | `uninitialized-local` | [Uninitialized local variables](https://github.com/crytic/slither/wiki/Detector-Documentation#uninitialized-local-variables) | Medium | Medium
47 | `unused-return` | [Unused return values](https://github.com/crytic/slither/wiki/Detector-Documentation#unused-return) | Medium | Medium
48 | `incorrect-modifier` | [Modifiers that can return the default value](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-modifier) | Low | High
49 | `shadowing-builtin` | [Built-in symbol shadowing](https://github.com/crytic/slither/wiki/Detector-Documentation#builtin-symbol-shadowing) | Low | High
50 | `shadowing-local` | [Local variables shadowing](https://github.com/crytic/slither/wiki/Detector-Documentation#local-variable-shadowing) | Low | High
51 | `uninitialized-fptr-cst` | [Uninitialized function pointer calls in constructors](https://github.com/crytic/slither/wiki/Detector-Documentation#uninitialized-function-pointers-in-constructors) | Low | High
52 | `variable-scope` | [Local variables used prior their declaration](https://github.com/crytic/slither/wiki/Detector-Documentation#pre-declaration-usage-of-local-variables) | Low | High
53 | `void-cst` | [Constructor called not implemented](https://github.com/crytic/slither/wiki/Detector-Documentation#void-constructor) | Low | High
54 | `calls-loop` | [Multiple calls in a loop](https://github.com/crytic/slither/wiki/Detector-Documentation/#calls-inside-a-loop) | Low | Medium
55 | `events-access` | [Missing Events Access Control](https://github.com/crytic/slither/wiki/Detector-Documentation#missing-events-access-control) | Low | Medium
56 | `events-maths` | [Missing Events Arithmetic](https://github.com/crytic/slither/wiki/Detector-Documentation#missing-events-arithmetic) | Low | Medium
57 | `incorrect-unary` | [Dangerous unary expressions](https://github.com/crytic/slither/wiki/Detector-Documentation#dangerous-unary-expressions) | Low | Medium
58 | `missing-zero-check` | [Missing Zero Address Validation](https://github.com/crytic/slither/wiki/Detector-Documentation#missing-zero-address-validation) | Low | Medium
59 | `reentrancy-benign` | [Benign reentrancy vulnerabilities](https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-2) | Low | Medium
60 | `reentrancy-events` | [Reentrancy vulnerabilities leading to out-of-order Events](https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-3) | Low | Medium
61 | `timestamp` | [Dangerous usage of `block.timestamp`](https://github.com/crytic/slither/wiki/Detector-Documentation#block-timestamp) | Low | Medium
62 | `assembly` | [Assembly usage](https://github.com/crytic/slither/wiki/Detector-Documentation#assembly-usage) | Informational | High
63 | `assert-state-change` | [Assert state change](https://github.com/crytic/slither/wiki/Detector-Documentation#assert-state-change) | Informational | High
64 | `boolean-equal` | [Comparison to boolean constant](https://github.com/crytic/slither/wiki/Detector-Documentation#boolean-equality) | Informational | High
65 | `cyclomatic-complexity` | [Detects functions with high (> 11) cyclomatic complexity](https://github.com/crytic/slither/wiki/Detector-Documentation#cyclomatic-complexity) | Informational | High
66 | `deprecated-standards` | [Deprecated Solidity Standards](https://github.com/crytic/slither/wiki/Detector-Documentation#deprecated-standards) | Informational | High
67 | `erc20-indexed` | [Un-indexed ERC20 event parameters](https://github.com/crytic/slither/wiki/Detector-Documentation#unindexed-erc20-event-parameters) | Informational | High
68 | `function-init-state` | [Function initializing state variables](https://github.com/crytic/slither/wiki/Detector-Documentation#function-initializing-state) | Informational | High
69 | `incorrect-using-for` | [Detects using-for statement usage when no function from a given library matches a given type](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-using-for-usage) | Informational | High
70 | `low-level-calls` | [Low level calls](https://github.com/crytic/slither/wiki/Detector-Documentation#low-level-calls) | Informational | High
71 | `missing-inheritance` | [Missing inheritance](https://github.com/crytic/slither/wiki/Detector-Documentation#missing-inheritance) | Informational | High
72 | `naming-convention` | [Conformity to Solidity naming conventions](https://github.com/crytic/slither/wiki/Detector-Documentation#conformance-to-solidity-naming-conventions) | Informational | High
73 | `pragma` | [If different pragma directives are used](https://github.com/crytic/slither/wiki/Detector-Documentation#different-pragma-directives-are-used) | Informational | High
74 | `redundant-statements` | [Redundant statements](https://github.com/crytic/slither/wiki/Detector-Documentation#redundant-statements) | Informational | High
75 | `solc-version` | [Incorrect Solidity version](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-versions-of-solidity) | Informational | High
76 | `unimplemented-functions` | [Unimplemented functions](https://github.com/crytic/slither/wiki/Detector-Documentation#unimplemented-functions) | Informational | High
77 | `unused-state` | [Unused state variables](https://github.com/crytic/slither/wiki/Detector-Documentation#unused-state-variable) | Informational | High
78 | `costly-loop` | [Costly operations in a loop](https://github.com/crytic/slither/wiki/Detector-Documentation#costly-operations-inside-a-loop) | Informational | Medium
79 | `dead-code` | [Functions that are not used](https://github.com/crytic/slither/wiki/Detector-Documentation#dead-code) | Informational | Medium
80 | `reentrancy-unlimited-gas` | [Reentrancy vulnerabilities through send and transfer](https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-4) | Informational | Medium
81 | `similar-names` | [Variable names are too similar](https://github.com/crytic/slither/wiki/Detector-Documentation#variable-names-too-similar) | Informational | Medium
82 | `too-many-digits` | [Conformance to numeric notation best practices](https://github.com/crytic/slither/wiki/Detector-Documentation#too-many-digits) | Informational | Medium
83 | `cache-array-length` | [Detects `for` loops that use `length` member of some storage array in their loop condition and don't modify it.](https://github.com/crytic/slither/wiki/Detector-Documentation#cache-array-length) | Optimization | High
84 | `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
85 | `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
86 | `immutable-states` | [State variables that could be declared immutable](https://github.com/crytic/slither/wiki/Detector-Documentation#state-variables-that-could-be-declared-immutable) | Optimization | High
87 | `var-read-using-this` | [Contract reads its own variable using `this`](https://github.com/crytic/slither/wiki/Detector-Documentation#public-variable-read-in-external-context) | Optimization | High
22 | `incorrect-exp` | [Incorrect exponentiation](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-exponentiation) | High | Medium
23 | `incorrect-return` | [If a `return` is incorrectly used in assembly mode.](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-assembly-return) | High | Medium
24 | `msg-value-loop` | [msg.value inside a loop](https://github.com/crytic/slither/wiki/Detector-Documentation/#msgvalue-inside-a-loop) | High | Medium
25 | `reentrancy-eth` | [Reentrancy vulnerabilities (theft of ethers)](https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities) | High | Medium
26 | `return-leave` | [If a `return` is used instead of a `leave`.](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-assembly-return) | High | Medium
27 | `storage-array` | [Signed storage integer array compiler bug](https://github.com/crytic/slither/wiki/Detector-Documentation#storage-signed-integer-array) | High | Medium
28 | `unchecked-transfer` | [Unchecked tokens transfer](https://github.com/crytic/slither/wiki/Detector-Documentation#unchecked-transfer) | High | Medium
29 | `weak-prng` | [Weak PRNG](https://github.com/crytic/slither/wiki/Detector-Documentation#weak-PRNG) | High | Medium
30 | `domain-separator-collision` | [Detects ERC20 tokens that have a function whose signature collides with EIP-2612's DOMAIN_SEPARATOR()](https://github.com/crytic/slither/wiki/Detector-Documentation#domain-separator-collision) | Medium | High
31 | `enum-conversion` | [Detect dangerous enum conversion](https://github.com/crytic/slither/wiki/Detector-Documentation#dangerous-enum-conversion) | Medium | High
32 | `erc20-interface` | [Incorrect ERC20 interfaces](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-erc20-interface) | Medium | High
33 | `erc721-interface` | [Incorrect ERC721 interfaces](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-erc721-interface) | Medium | High
34 | `incorrect-equality` | [Dangerous strict equalities](https://github.com/crytic/slither/wiki/Detector-Documentation#dangerous-strict-equalities) | Medium | High
35 | `locked-ether` | [Contracts that lock ether](https://github.com/crytic/slither/wiki/Detector-Documentation#contracts-that-lock-ether) | Medium | High
36 | `mapping-deletion` | [Deletion on mapping containing a structure](https://github.com/crytic/slither/wiki/Detector-Documentation#deletion-on-mapping-containing-a-structure) | Medium | High
37 | `shadowing-abstract` | [State variables shadowing from abstract contracts](https://github.com/crytic/slither/wiki/Detector-Documentation#state-variable-shadowing-from-abstract-contracts) | Medium | High
38 | `tautological-compare` | [Comparing a variable to itself always returns true or false, depending on comparison](https://github.com/crytic/slither/wiki/Detector-Documentation#tautological-compare) | Medium | High
39 | `tautology` | [Tautology or contradiction](https://github.com/crytic/slither/wiki/Detector-Documentation#tautology-or-contradiction) | Medium | High
40 | `write-after-write` | [Unused write](https://github.com/crytic/slither/wiki/Detector-Documentation#write-after-write) | Medium | High
41 | `boolean-cst` | [Misuse of Boolean constant](https://github.com/crytic/slither/wiki/Detector-Documentation#misuse-of-a-boolean-constant) | Medium | Medium
42 | `constant-function-asm` | [Constant functions using assembly code](https://github.com/crytic/slither/wiki/Detector-Documentation#constant-functions-using-assembly-code) | Medium | Medium
43 | `constant-function-state` | [Constant functions changing the state](https://github.com/crytic/slither/wiki/Detector-Documentation#constant-functions-changing-the-state) | Medium | Medium
44 | `divide-before-multiply` | [Imprecise arithmetic operations order](https://github.com/crytic/slither/wiki/Detector-Documentation#divide-before-multiply) | Medium | Medium
45 | `reentrancy-no-eth` | [Reentrancy vulnerabilities (no theft of ethers)](https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-1) | Medium | Medium
46 | `reused-constructor` | [Reused base constructor](https://github.com/crytic/slither/wiki/Detector-Documentation#reused-base-constructors) | Medium | Medium
47 | `tx-origin` | [Dangerous usage of `tx.origin`](https://github.com/crytic/slither/wiki/Detector-Documentation#dangerous-usage-of-txorigin) | Medium | Medium
48 | `unchecked-lowlevel` | [Unchecked low-level calls](https://github.com/crytic/slither/wiki/Detector-Documentation#unchecked-low-level-calls) | Medium | Medium
49 | `unchecked-send` | [Unchecked send](https://github.com/crytic/slither/wiki/Detector-Documentation#unchecked-send) | Medium | Medium
50 | `uninitialized-local` | [Uninitialized local variables](https://github.com/crytic/slither/wiki/Detector-Documentation#uninitialized-local-variables) | Medium | Medium
51 | `unused-return` | [Unused return values](https://github.com/crytic/slither/wiki/Detector-Documentation#unused-return) | Medium | Medium
52 | `incorrect-modifier` | [Modifiers that can return the default value](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-modifier) | Low | High
53 | `shadowing-builtin` | [Built-in symbol shadowing](https://github.com/crytic/slither/wiki/Detector-Documentation#builtin-symbol-shadowing) | Low | High
54 | `shadowing-local` | [Local variables shadowing](https://github.com/crytic/slither/wiki/Detector-Documentation#local-variable-shadowing) | Low | High
55 | `uninitialized-fptr-cst` | [Uninitialized function pointer calls in constructors](https://github.com/crytic/slither/wiki/Detector-Documentation#uninitialized-function-pointers-in-constructors) | Low | High
56 | `variable-scope` | [Local variables used prior their declaration](https://github.com/crytic/slither/wiki/Detector-Documentation#pre-declaration-usage-of-local-variables) | Low | High
57 | `void-cst` | [Constructor called not implemented](https://github.com/crytic/slither/wiki/Detector-Documentation#void-constructor) | Low | High
58 | `calls-loop` | [Multiple calls in a loop](https://github.com/crytic/slither/wiki/Detector-Documentation/#calls-inside-a-loop) | Low | Medium
59 | `events-access` | [Missing Events Access Control](https://github.com/crytic/slither/wiki/Detector-Documentation#missing-events-access-control) | Low | Medium
60 | `events-maths` | [Missing Events Arithmetic](https://github.com/crytic/slither/wiki/Detector-Documentation#missing-events-arithmetic) | Low | Medium
61 | `incorrect-unary` | [Dangerous unary expressions](https://github.com/crytic/slither/wiki/Detector-Documentation#dangerous-unary-expressions) | Low | Medium
62 | `missing-zero-check` | [Missing Zero Address Validation](https://github.com/crytic/slither/wiki/Detector-Documentation#missing-zero-address-validation) | Low | Medium
63 | `reentrancy-benign` | [Benign reentrancy vulnerabilities](https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-2) | Low | Medium
64 | `reentrancy-events` | [Reentrancy vulnerabilities leading to out-of-order Events](https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-3) | Low | Medium
65 | `return-bomb` | [A low level callee may consume all callers gas unexpectedly.](https://github.com/crytic/slither/wiki/Detector-Documentation#return-bomb) | Low | Medium
66 | `timestamp` | [Dangerous usage of `block.timestamp`](https://github.com/crytic/slither/wiki/Detector-Documentation#block-timestamp) | Low | Medium
67 | `assembly` | [Assembly usage](https://github.com/crytic/slither/wiki/Detector-Documentation#assembly-usage) | Informational | High
68 | `assert-state-change` | [Assert state change](https://github.com/crytic/slither/wiki/Detector-Documentation#assert-state-change) | Informational | High
69 | `boolean-equal` | [Comparison to boolean constant](https://github.com/crytic/slither/wiki/Detector-Documentation#boolean-equality) | Informational | High
70 | `cyclomatic-complexity` | [Detects functions with high (> 11) cyclomatic complexity](https://github.com/crytic/slither/wiki/Detector-Documentation#cyclomatic-complexity) | Informational | High
71 | `deprecated-standards` | [Deprecated Solidity Standards](https://github.com/crytic/slither/wiki/Detector-Documentation#deprecated-standards) | Informational | High
72 | `erc20-indexed` | [Un-indexed ERC20 event parameters](https://github.com/crytic/slither/wiki/Detector-Documentation#unindexed-erc20-event-parameters) | Informational | High
73 | `function-init-state` | [Function initializing state variables](https://github.com/crytic/slither/wiki/Detector-Documentation#function-initializing-state) | Informational | High
74 | `incorrect-using-for` | [Detects using-for statement usage when no function from a given library matches a given type](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-using-for-usage) | Informational | High
75 | `low-level-calls` | [Low level calls](https://github.com/crytic/slither/wiki/Detector-Documentation#low-level-calls) | Informational | High
76 | `missing-inheritance` | [Missing inheritance](https://github.com/crytic/slither/wiki/Detector-Documentation#missing-inheritance) | Informational | High
77 | `naming-convention` | [Conformity to Solidity naming conventions](https://github.com/crytic/slither/wiki/Detector-Documentation#conformance-to-solidity-naming-conventions) | Informational | High
78 | `pragma` | [If different pragma directives are used](https://github.com/crytic/slither/wiki/Detector-Documentation#different-pragma-directives-are-used) | Informational | High
79 | `redundant-statements` | [Redundant statements](https://github.com/crytic/slither/wiki/Detector-Documentation#redundant-statements) | Informational | High
80 | `solc-version` | [Incorrect Solidity version](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-versions-of-solidity) | Informational | High
81 | `unimplemented-functions` | [Unimplemented functions](https://github.com/crytic/slither/wiki/Detector-Documentation#unimplemented-functions) | Informational | High
82 | `unused-state` | [Unused state variables](https://github.com/crytic/slither/wiki/Detector-Documentation#unused-state-variable) | Informational | High
83 | `costly-loop` | [Costly operations in a loop](https://github.com/crytic/slither/wiki/Detector-Documentation#costly-operations-inside-a-loop) | Informational | Medium
84 | `dead-code` | [Functions that are not used](https://github.com/crytic/slither/wiki/Detector-Documentation#dead-code) | Informational | Medium
85 | `reentrancy-unlimited-gas` | [Reentrancy vulnerabilities through send and transfer](https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-4) | Informational | Medium
86 | `similar-names` | [Variable names are too similar](https://github.com/crytic/slither/wiki/Detector-Documentation#variable-names-too-similar) | Informational | Medium
87 | `too-many-digits` | [Conformance to numeric notation best practices](https://github.com/crytic/slither/wiki/Detector-Documentation#too-many-digits) | Informational | Medium
88 | `cache-array-length` | [Detects `for` loops that use `length` member of some storage array in their loop condition and don't modify it.](https://github.com/crytic/slither/wiki/Detector-Documentation#cache-array-length) | Optimization | High
89 | `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
90 | `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
91 | `immutable-states` | [State variables that could be declared immutable](https://github.com/crytic/slither/wiki/Detector-Documentation#state-variables-that-could-be-declared-immutable) | Optimization | High
92 | `var-read-using-this` | [Contract reads its own variable using `this`](https://github.com/crytic/slither/wiki/Detector-Documentation#public-variable-read-in-external-context) | Optimization | High
For more information, see
@ -205,12 +211,14 @@ For more information, see
## Printers
### Quick Review Printers
* `human-summary`: [Print a human-readable summary of the contracts](https://github.com/trailofbits/slither/wiki/Printer-documentation#human-summary)
* `inheritance-graph`: [Export the inheritance graph of each contract to a dot file](https://github.com/trailofbits/slither/wiki/Printer-documentation#inheritance-graph)
* `contract-summary`: [Print a summary of the contracts](https://github.com/trailofbits/slither/wiki/Printer-documentation#contract-summary)
* `loc`: [Count the total number lines of code (LOC), source lines of code (SLOC), and comment lines of code (CLOC) found in source files (SRC), dependencies (DEP), and test files (TEST).](https://github.com/trailofbits/slither/wiki/Printer-documentation#loc)
### In-Depth Review Printers
* `call-graph`: [Export the call-graph of the contracts to a dot file](https://github.com/trailofbits/slither/wiki/Printer-documentation#call-graph)
* `cfg`: [Export the CFG of each functions](https://github.com/trailofbits/slither/wiki/Printer-documentation#cfg)
* `function-summary`: [Print a summary of the functions](https://github.com/trailofbits/slither/wiki/Printer-documentation#function-summary)

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

@ -15,8 +15,8 @@ setup(
"packaging",
"prettytable>=3.3.0",
"pycryptodome>=3.4.6",
"crytic-compile>=0.3.3,<0.4.0",
# "crytic-compile@git+https://github.com/crytic/crytic-compile.git@dev#egg=crytic-compile",
# "crytic-compile>=0.3.1,<0.4.0",
"crytic-compile@git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile",
"web3>=6.0.0",
"eth-abi>=4.0.0",
"eth-typing>=3.0.0",

@ -79,6 +79,11 @@ def process_single(
ast = "--ast-json"
slither = Slither(target, ast_format=ast, **vars(args))
if args.sarif_input:
slither.sarif_input = args.sarif_input
if args.sarif_triage:
slither.sarif_triage = args.sarif_triage
return _process(slither, detector_classes, printer_classes)
@ -442,7 +447,7 @@ def parse_args(
group_checklist.add_argument(
"--checklist-limit",
help="Limite the number of results per detector in the markdown file",
help="Limit the number of results per detector in the markdown file",
action="store",
default="",
)
@ -469,6 +474,20 @@ def parse_args(
default=defaults_flag_in_config["sarif"],
)
group_misc.add_argument(
"--sarif-input",
help="Sarif input (beta)",
action="store",
default=defaults_flag_in_config["sarif_input"],
)
group_misc.add_argument(
"--sarif-triage",
help="Sarif triage (beta)",
action="store",
default=defaults_flag_in_config["sarif_triage"],
)
group_misc.add_argument(
"--json-types",
help="Comma-separated list of result types to output to JSON, defaults to "
@ -870,12 +889,6 @@ def main_impl(
logging.error(red(output_error))
logging.error("Please report an issue to https://github.com/crytic/slither/issues")
except Exception: # pylint: disable=broad-except
output_error = traceback.format_exc()
traceback.print_exc()
logging.error(f"Error in {args.filename}") # pylint: disable=logging-fstring-interpolation
logging.error(output_error)
# If we are outputting JSON, capture the redirected output and disable the redirect to output the final JSON.
if outputting_json:
if "console" in args.json_types:

@ -74,6 +74,7 @@ class NodeType(Enum):
IF = "IF"
VARIABLE = "NEW VARIABLE" # Variable declaration
ASSEMBLY = "INLINE ASM"
ENDASSEMBLY = "END INLINE ASM"
IFLOOP = "IF_LOOP"
# Nodes where control flow merges
@ -193,6 +194,8 @@ class Node(SourceMapping): # pylint: disable=too-many-public-methods
self.file_scope: "FileScope" = file_scope
self._function: Optional["Function"] = None
self._is_reachable: bool = False
###################################################################################
###################################################################################
# region General's properties
@ -234,6 +237,13 @@ class Node(SourceMapping): # pylint: disable=too-many-public-methods
def function(self) -> "Function":
return self._function
@property
def is_reachable(self) -> bool:
return self._is_reachable
def set_is_reachable(self, new_is_reachable: bool) -> None:
self._is_reachable = new_is_reachable
# endregion
###################################################################################
###################################################################################

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

@ -18,3 +18,6 @@ from .structure_top_level import StructureTopLevel
from .function_contract import FunctionContract
from .function_top_level import FunctionTopLevel
from .custom_error_contract import CustomErrorContract
from .custom_error_top_level import CustomErrorTopLevel
from .custom_error import CustomError
from .solidity_import_placeholder import SolidityImportPlaceHolder

@ -45,6 +45,7 @@ if TYPE_CHECKING:
from slither.core.compilation_unit import SlitherCompilationUnit
from slither.core.scope.scope import FileScope
from slither.core.cfg.node import Node
from slither.core.solidity_types import TypeAliasContract
LOGGER = logging.getLogger("Contract")
@ -77,10 +78,13 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
# do not contain private variables inherited from contract
self._variables: Dict[str, "StateVariable"] = {}
self._variables_ordered: List["StateVariable"] = []
# Reference id -> variable declaration (only available for compact AST)
self._state_variables_by_ref_id: Dict[int, "StateVariable"] = {}
self._modifiers: Dict[str, "Modifier"] = {}
self._functions: Dict[str, "FunctionContract"] = {}
self._linearizedBaseContracts: List[int] = []
self._custom_errors: Dict[str, "CustomErrorContract"] = {}
self._type_aliases: Dict[str, "TypeAliasContract"] = {}
# The only str is "*"
self._using_for: Dict[USING_FOR_KEY, USING_FOR_ITEM] = {}
@ -136,7 +140,7 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
@property
def id(self) -> int:
"""Unique id."""
assert self._id
assert self._id is not None
return self._id
@id.setter
@ -364,12 +368,50 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
def custom_errors_as_dict(self) -> Dict[str, "CustomErrorContract"]:
return self._custom_errors
# endregion
###################################################################################
###################################################################################
# region Custom Errors
###################################################################################
###################################################################################
@property
def type_aliases(self) -> List["TypeAliasContract"]:
"""
list(TypeAliasContract): List of the contract's custom errors
"""
return list(self._type_aliases.values())
@property
def type_aliases_inherited(self) -> List["TypeAliasContract"]:
"""
list(TypeAliasContract): List of the inherited custom errors
"""
return [s for s in self.type_aliases if s.contract != self]
@property
def type_aliases_declared(self) -> List["TypeAliasContract"]:
"""
list(TypeAliasContract): List of the custom errors declared within the contract (not inherited)
"""
return [s for s in self.type_aliases if s.contract == self]
@property
def type_aliases_as_dict(self) -> Dict[str, "TypeAliasContract"]:
return self._type_aliases
# endregion
###################################################################################
###################################################################################
# region Variables
###################################################################################
###################################################################################
@property
def state_variables_by_ref_id(self) -> Dict[int, "StateVariable"]:
"""
Returns the state variables by reference id (only available for compact AST).
"""
return self._state_variables_by_ref_id
@property
def variables(self) -> List["StateVariable"]:
@ -861,7 +903,7 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
Returns:
StateVariable
"""
return next((v for v in self.state_variables if v.name == canonical_name), None)
return next((v for v in self.state_variables if v.canonical_name == canonical_name), None)
def get_structure_from_name(self, structure_name: str) -> Optional["StructureContract"]:
"""

@ -16,3 +16,7 @@ class CustomErrorContract(CustomError, ContractLevel):
:return:
"""
return self.contract == contract
@property
def canonical_name(self) -> str:
return self.contract.name + "." + self.full_name

@ -12,3 +12,7 @@ class CustomErrorTopLevel(CustomError, TopLevel):
def __init__(self, compilation_unit: "SlitherCompilationUnit", scope: "FileScope") -> None:
super().__init__(compilation_unit)
self.file_scope: "FileScope" = scope
@property
def canonical_name(self) -> str:
return self.full_name

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

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

@ -9,9 +9,21 @@ if TYPE_CHECKING:
def intersection_predecessor(node: "Node") -> Set["Node"]:
if not node.fathers:
return set()
# Revert PR1984
ret = node.fathers[0].dominators
for pred in node.fathers[1:]:
ret = ret.intersection(pred.dominators)
# if not any(father.is_reachable for father in node.fathers):
# return set()
#
# ret = set()
# for pred in node.fathers:
# ret = ret.union(pred.dominators)
#
# for pred in node.fathers:
# if pred.is_reachable:
# ret = ret.intersection(pred.dominators)
return ret
@ -84,6 +96,9 @@ def compute_dominance_frontier(nodes: List["Node"]) -> None:
for node in nodes:
if len(node.fathers) >= 2:
for father in node.fathers:
# Revert PR1984
# if not father.is_reachable:
# continue
runner = father
# Corner case: if there is a if without else
# we need to add update the conditional node

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

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

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

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

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

@ -106,8 +106,6 @@ class UnaryOperation(Expression):
UnaryOperationType.MINUSMINUS_PRE,
UnaryOperationType.PLUSPLUS_POST,
UnaryOperationType.MINUSMINUS_POST,
UnaryOperationType.PLUS_PRE,
UnaryOperationType.MINUS_PRE,
]:
expression.set_lvalue()

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

@ -21,6 +21,7 @@ from slither.core.declarations.top_level import TopLevel
from slither.core.source_mapping.source_mapping import SourceMapping, Source
from slither.slithir.variables import Constant
from slither.utils.colors import red
from slither.utils.sarif import read_triage_info
from slither.utils.source_mapping import get_definition, get_references, get_implementation
logger = logging.getLogger("Slither")
@ -48,6 +49,10 @@ class SlitherCore(Context):
self._source_code_to_line: Optional[Dict[str, List[str]]] = None
self._previous_results_filename: str = "slither.db.json"
# TODO: add cli flag to set these variables
self.sarif_input: str = "export.sarif"
self.sarif_triage: str = "export.sarif.sarifexplorer"
self._results_to_hide: List = []
self._previous_results: List = []
# From triaged result
@ -96,6 +101,8 @@ class SlitherCore(Context):
# If true, partial analysis is allowed
self.no_fail = False
self.skip_data_dependency = False
@property
def compilation_units(self) -> List[SlitherCompilationUnit]:
return list(self._compilation_units)
@ -444,6 +451,8 @@ class SlitherCore(Context):
return True
def load_previous_results(self) -> None:
self.load_previous_results_from_sarif()
filename = self._previous_results_filename
try:
if os.path.isfile(filename):
@ -453,9 +462,24 @@ class SlitherCore(Context):
for r in self._previous_results:
if "id" in r:
self._previous_results_ids.add(r["id"])
except json.decoder.JSONDecodeError:
logger.error(red(f"Impossible to decode {filename}. Consider removing the file"))
def load_previous_results_from_sarif(self) -> None:
sarif = pathlib.Path(self.sarif_input)
triage = pathlib.Path(self.sarif_triage)
if not sarif.exists():
return
if not triage.exists():
return
triaged = read_triage_info(sarif, triage)
for id_triaged in triaged:
self._previous_results_ids.add(id_triaged)
def write_results_to_hide(self) -> None:
if not self._results_to_hide:
return

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

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

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

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

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

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

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

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

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

@ -17,7 +17,7 @@ class CacheArrayLength(AbstractDetector):
ARGUMENT = "cache-array-length"
HELP = (
"Detects `for` loops that use `length` member of some storage array in their loop condition and don't "
"modify it. "
"modify it."
)
IMPACT = DetectorClassification.OPTIMIZATION
CONFIDENCE = DetectorClassification.HIGH
@ -216,9 +216,8 @@ contract C
for usage in non_optimal_array_len_usages:
info = [
"Loop condition ",
f"`{usage.source_mapping.content}` ",
f"({usage.source_mapping}) ",
"should use cached array length instead of referencing `length` member "
usage,
" should use cached array length instead of referencing `length` member "
"of the storage array.\n ",
]
res = self.generate_result(info)

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

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

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

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

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

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

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

@ -4,7 +4,7 @@ from typing import Dict, List, Set, Tuple, NamedTuple, Union
from slither.analyses.data_dependency.data_dependency import is_dependent
from slither.core.cfg.node import Node
from slither.core.declarations import Enum, Function
from slither.core.declarations import Enum, Function, Contract
from slither.core.declarations.solidity_variables import (
SolidityVariableComposed,
SolidityFunction,
@ -30,7 +30,7 @@ from slither.slithir.operations import (
TypeConversion,
)
from slither.slithir.operations.binary import Binary
from slither.slithir.variables import Constant
from slither.slithir.variables import Constant, ReferenceVariable
from slither.utils.output import Output
from slither.visitors.expression.constants_folding import ConstantFolding, NotConstant
@ -43,9 +43,9 @@ def _get_name(f: Union[Function, Variable]) -> str:
return f.solidity_signature
def _extract_payable(slither: SlitherCore) -> Dict[str, List[str]]:
def _extract_payable(contracts: List[Contract]) -> Dict[str, List[str]]:
ret: Dict[str, List[str]] = {}
for contract in slither.contracts:
for contract in contracts:
payable_functions = [_get_name(f) for f in contract.functions_entry_points if f.payable]
if payable_functions:
ret[contract.name] = payable_functions
@ -53,10 +53,10 @@ def _extract_payable(slither: SlitherCore) -> Dict[str, List[str]]:
def _extract_solidity_variable_usage(
slither: SlitherCore, sol_var: SolidityVariable
contracts: List[Contract], sol_var: SolidityVariable
) -> Dict[str, List[str]]:
ret: Dict[str, List[str]] = {}
for contract in slither.contracts:
for contract in contracts:
functions_using_sol_var = []
for f in contract.functions_entry_points:
for v in f.all_solidity_variables_read():
@ -114,9 +114,9 @@ def _is_constant(f: Function) -> bool: # pylint: disable=too-many-branches
return True
def _extract_constant_functions(slither: SlitherCore) -> Dict[str, List[str]]:
def _extract_constant_functions(contracts: List[Contract]) -> Dict[str, List[str]]:
ret: Dict[str, List[str]] = {}
for contract in slither.contracts:
for contract in contracts:
cst_functions = [_get_name(f) for f in contract.functions_entry_points if _is_constant(f)]
cst_functions += [
v.solidity_signature for v in contract.state_variables if v.visibility in ["public"]
@ -126,15 +126,24 @@ def _extract_constant_functions(slither: SlitherCore) -> Dict[str, List[str]]:
return ret
def _extract_assert(slither: SlitherCore) -> Dict[str, List[str]]:
ret: Dict[str, List[str]] = {}
for contract in slither.contracts:
functions_using_assert = []
def _extract_assert(contracts: List[Contract]) -> Dict[str, Dict[str, List[Dict]]]:
"""
Return the list of contract -> function name -> List(source mapping of the assert))
Args:
contracts: list of contracts
Returns:
"""
ret: Dict[str, Dict[str, List[Dict]]] = {}
for contract in contracts:
functions_using_assert: Dict[str, List[Dict]] = defaultdict(list)
for f in contract.functions_entry_points:
for v in f.all_solidity_calls():
if v == SolidityFunction("assert(bool)"):
functions_using_assert.append(_get_name(f))
break
for node in f.all_nodes():
if SolidityFunction("assert(bool)") in node.solidity_calls and node.source_mapping:
func_name = _get_name(f)
functions_using_assert[func_name].append(node.source_mapping.to_json())
if functions_using_assert:
ret[contract.name] = functions_using_assert
return ret
@ -208,19 +217,20 @@ def _extract_constants_from_irs( # pylint: disable=too-many-branches,too-many-n
except ValueError: # index could fail; should never happen in working solidity code
pass
for r in ir.read:
var_read = r.points_to_origin if isinstance(r, ReferenceVariable) else r
# Do not report struct_name in a.struct_name
if isinstance(ir, Member):
continue
if isinstance(r, Constant):
all_cst_used.append(ConstantValue(str(r.value), str(r.type)))
if isinstance(r, StateVariable):
if r.node_initialization:
if r.node_initialization.irs:
if r.node_initialization in context_explored:
if isinstance(var_read, Constant):
all_cst_used.append(ConstantValue(str(var_read.value), str(var_read.type)))
if isinstance(var_read, StateVariable):
if var_read.node_initialization:
if var_read.node_initialization.irs:
if var_read.node_initialization in context_explored:
continue
context_explored.add(r.node_initialization)
context_explored.add(var_read.node_initialization)
_extract_constants_from_irs(
r.node_initialization.irs,
var_read.node_initialization.irs,
all_cst_used,
all_cst_used_in_binary,
context_explored,
@ -228,13 +238,13 @@ def _extract_constants_from_irs( # pylint: disable=too-many-branches,too-many-n
def _extract_constants(
slither: SlitherCore,
contracts: List[Contract],
) -> Tuple[Dict[str, Dict[str, List]], Dict[str, Dict[str, Dict]]]:
# contract -> function -> [ {"value": value, "type": type} ]
ret_cst_used: Dict[str, Dict[str, List[ConstantValue]]] = defaultdict(dict)
# contract -> function -> binary_operand -> [ {"value": value, "type": type ]
ret_cst_used_in_binary: Dict[str, Dict[str, Dict[str, List[ConstantValue]]]] = defaultdict(dict)
for contract in slither.contracts:
for contract in contracts:
for function in contract.functions_entry_points:
all_cst_used: List = []
all_cst_used_in_binary: Dict = defaultdict(list)
@ -260,11 +270,11 @@ def _extract_constants(
def _extract_function_relations(
slither: SlitherCore,
contracts: List[Contract],
) -> Dict[str, Dict[str, Dict[str, List[str]]]]:
# contract -> function -> [functions]
ret: Dict[str, Dict[str, Dict[str, List[str]]]] = defaultdict(dict)
for contract in slither.contracts:
for contract in contracts:
ret[contract.name] = defaultdict(dict)
written = {
_get_name(function): function.all_state_variables_written()
@ -288,14 +298,14 @@ def _extract_function_relations(
return ret
def _have_external_calls(slither: SlitherCore) -> Dict[str, List[str]]:
def _have_external_calls(contracts: List[Contract]) -> Dict[str, List[str]]:
"""
Detect the functions with external calls
:param slither:
:return:
"""
ret: Dict[str, List[str]] = defaultdict(list)
for contract in slither.contracts:
for contract in contracts:
for function in contract.functions_entry_points:
if function.all_high_level_calls() or function.all_low_level_calls():
ret[contract.name].append(_get_name(function))
@ -304,14 +314,14 @@ def _have_external_calls(slither: SlitherCore) -> Dict[str, List[str]]:
return ret
def _use_balance(slither: SlitherCore) -> Dict[str, List[str]]:
def _use_balance(contracts: List[Contract]) -> Dict[str, List[str]]:
"""
Detect the functions with external calls
:param slither:
:return:
"""
ret: Dict[str, List[str]] = defaultdict(list)
for contract in slither.contracts:
for contract in contracts:
for function in contract.functions_entry_points:
for ir in function.all_slithir_operations():
if isinstance(ir, SolidityCall) and ir.function == SolidityFunction(
@ -323,25 +333,25 @@ def _use_balance(slither: SlitherCore) -> Dict[str, List[str]]:
return ret
def _with_fallback(slither: SlitherCore) -> Set[str]:
def _with_fallback(contracts: List[Contract]) -> Set[str]:
ret: Set[str] = set()
for contract in slither.contracts:
for contract in contracts:
for function in contract.functions_entry_points:
if function.is_fallback:
ret.add(contract.name)
return ret
def _with_receive(slither: SlitherCore) -> Set[str]:
def _with_receive(contracts: List[Contract]) -> Set[str]:
ret: Set[str] = set()
for contract in slither.contracts:
for contract in contracts:
for function in contract.functions_entry_points:
if function.is_receive:
ret.add(contract.name)
return ret
def _call_a_parameter(slither: SlitherCore) -> Dict[str, List[Dict]]:
def _call_a_parameter(slither: SlitherCore, contracts: List[Contract]) -> Dict[str, List[Dict]]:
"""
Detect the functions with external calls
:param slither:
@ -349,7 +359,7 @@ def _call_a_parameter(slither: SlitherCore) -> Dict[str, List[Dict]]:
"""
# contract -> [ (function, idx, interface_called) ]
ret: Dict[str, List[Dict]] = defaultdict(list)
for contract in slither.contracts: # pylint: disable=too-many-nested-blocks
for contract in contracts: # pylint: disable=too-many-nested-blocks
for function in contract.functions_entry_points:
try:
for ir in function.all_slithir_operations():
@ -395,40 +405,40 @@ class Echidna(AbstractPrinter):
_filename(string)
"""
payable = _extract_payable(self.slither)
contracts = self.slither.contracts
payable = _extract_payable(contracts)
timestamp = _extract_solidity_variable_usage(
self.slither, SolidityVariableComposed("block.timestamp")
contracts, SolidityVariableComposed("block.timestamp")
)
block_number = _extract_solidity_variable_usage(
self.slither, SolidityVariableComposed("block.number")
contracts, SolidityVariableComposed("block.number")
)
msg_sender = _extract_solidity_variable_usage(
self.slither, SolidityVariableComposed("msg.sender")
)
msg_gas = _extract_solidity_variable_usage(
self.slither, SolidityVariableComposed("msg.gas")
contracts, SolidityVariableComposed("msg.sender")
)
assert_usage = _extract_assert(self.slither)
cst_functions = _extract_constant_functions(self.slither)
(cst_used, cst_used_in_binary) = _extract_constants(self.slither)
msg_gas = _extract_solidity_variable_usage(contracts, SolidityVariableComposed("msg.gas"))
assert_usage = _extract_assert(contracts)
cst_functions = _extract_constant_functions(contracts)
(cst_used, cst_used_in_binary) = _extract_constants(contracts)
functions_relations = _extract_function_relations(self.slither)
functions_relations = _extract_function_relations(contracts)
constructors = {
contract.name: contract.constructor.full_name
for contract in self.slither.contracts
for contract in contracts
if contract.constructor
}
external_calls = _have_external_calls(self.slither)
external_calls = _have_external_calls(contracts)
call_parameters = _call_a_parameter(self.slither)
# call_parameters = _call_a_parameter(self.slither, contracts)
use_balance = _use_balance(self.slither)
use_balance = _use_balance(contracts)
with_fallback = list(_with_fallback(self.slither))
with_fallback = list(_with_fallback(contracts))
with_receive = list(_with_receive(self.slither))
with_receive = list(_with_receive(contracts))
d = {
"payable": payable,
@ -443,7 +453,7 @@ class Echidna(AbstractPrinter):
"functions_relations": functions_relations,
"constructors": constructors,
"have_external_calls": external_calls,
"call_a_parameter": call_parameters,
# "call_a_parameter": call_parameters,
"use_balance": use_balance,
"solc_versions": [unit.solc_version for unit in self.slither.compilation_units],
"with_fallback": with_fallback,

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -291,10 +291,10 @@ class ContractSolc(CallerContextExpression):
alias = item["name"]
alias_canonical = self._contract.name + "." + item["name"]
user_defined_type = TypeAliasContract(original_type, alias, self.underlying_contract)
user_defined_type.set_offset(item["src"], self.compilation_unit)
self._contract.file_scope.user_defined_types[alias] = user_defined_type
self._contract.file_scope.user_defined_types[alias_canonical] = user_defined_type
type_alias = TypeAliasContract(original_type, alias, self.underlying_contract)
type_alias.set_offset(item["src"], self.compilation_unit)
self._contract.type_aliases_as_dict[alias] = type_alias
self._contract.file_scope.type_aliases[alias_canonical] = type_alias
def _parse_struct(self, struct: Dict) -> None:
@ -319,7 +319,7 @@ class ContractSolc(CallerContextExpression):
ce.set_contract(self._contract)
ce.set_offset(custom_error["src"], self.compilation_unit)
ce_parser = CustomErrorSolc(ce, custom_error, self._slither_parser)
ce_parser = CustomErrorSolc(ce, custom_error, self, self._slither_parser)
self._contract.custom_errors_as_dict[ce.name] = ce
self._custom_errors_parser.append(ce_parser)
@ -357,6 +357,8 @@ class ContractSolc(CallerContextExpression):
self._variables_parser.append(var_parser)
assert var.name
if var_parser.reference_id is not None:
self._contract.state_variables_by_ref_id[var_parser.reference_id] = var
self._contract.variables_as_dict[var.name] = var
self._contract.add_variables_ordered([var])

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

@ -315,6 +315,9 @@ class FunctionSolc(CallerContextExpression):
self._remove_alone_endif()
if self._function.entry_point:
self._update_reachability(self._function.entry_point)
# endregion
###################################################################################
###################################################################################
@ -983,7 +986,9 @@ class FunctionSolc(CallerContextExpression):
# technically, entrypoint and exitpoint are YulNodes and we should be returning a NodeSolc here
# but they both expose an underlying_node so oh well
link_underlying_nodes(node, entrypoint)
node = exitpoint
end_assembly = self._new_node(NodeType.ENDASSEMBLY, statement["src"], scope)
link_underlying_nodes(exitpoint, end_assembly)
node = end_assembly
else:
asm_node = self._new_node(NodeType.ASSEMBLY, statement["src"], scope)
self._function.contains_assembly = True
@ -1100,6 +1105,13 @@ class FunctionSolc(CallerContextExpression):
node = self._parse_statement(statement, node, new_scope)
return node
def _update_reachability(self, node: Node) -> None:
if node.is_reachable:
return
node.set_is_reachable(True)
for son in node.sons:
self._update_reachability(son)
def _parse_cfg(self, cfg: Dict) -> None:
assert cfg[self.get_key()] == "Block"

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

@ -175,11 +175,12 @@ def parse_call(
called = parse_expression(children[0], caller_context)
arguments = [parse_expression(a, caller_context) for a in children[1::]]
if isinstance(called, SuperCallExpression):
if isinstance(called, SuperIdentifier):
sp = SuperCallExpression(called, arguments, type_return)
sp.set_offset(expression["src"], caller_context.compilation_unit)
return sp
call_expression = CallExpression(called, arguments, type_return)
names = expression["names"] if "names" in expression and len(expression["names"]) > 0 else None
call_expression = CallExpression(called, arguments, type_return, names=names)
call_expression.set_offset(src, caller_context.compilation_unit)
# Only available if the syntax {gas:, value:} was used
@ -486,13 +487,18 @@ def parse_expression(expression: Dict, caller_context: CallerContextExpression)
t = None
referenced_declaration = None
if caller_context.is_compact_ast:
value = expression["name"]
t = expression["typeDescriptions"]["typeString"]
if "referencedDeclaration" in expression:
referenced_declaration = expression["referencedDeclaration"]
else:
value = expression["attributes"]["value"]
if "type" in expression["attributes"]:
t = expression["attributes"]["type"]
if "referencedDeclaration" in expression["attributes"]:
referenced_declaration = expression["attributes"]["referencedDeclaration"]
if t:
found = re.findall(r"[struct|enum|function|modifier] \(([\[\] ()a-zA-Z0-9\.,_]*)\)", t)
@ -501,10 +507,6 @@ def parse_expression(expression: Dict, caller_context: CallerContextExpression)
value = value + "(" + found[0] + ")"
value = filter_name(value)
if "referencedDeclaration" in expression:
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)

@ -3,6 +3,8 @@ from typing import TYPE_CHECKING, Optional, Union, List, Tuple
from slither.core.declarations import Event, Enum, Structure
from slither.core.declarations.contract import Contract
from slither.core.declarations.custom_error import CustomError
from slither.core.declarations.custom_error_contract import CustomErrorContract
from slither.core.declarations.custom_error_top_level import CustomErrorTopLevel
from slither.core.declarations.function import Function
from slither.core.declarations.function_contract import FunctionContract
from slither.core.declarations.function_top_level import FunctionTopLevel
@ -54,10 +56,24 @@ def _find_variable_from_ref_declaration(
referenced_declaration: Optional[int],
all_contracts: List["Contract"],
all_functions: List["Function"],
function_parser: Optional["FunctionSolc"],
contract_declarer: Optional["Contract"],
) -> Optional[Union[Contract, Function]]:
"""
Reference declarations take the highest priority, but they are not available for legacy AST.
"""
if referenced_declaration is None:
return None
# id of the contracts is the referenced declaration
# We look for variable declared with the referencedDeclaration attribute
if function_parser is not None and referenced_declaration in function_parser.variables_renamed:
return function_parser.variables_renamed[referenced_declaration].underlying_variable
if (
contract_declarer is not None
and referenced_declaration in contract_declarer.state_variables_by_ref_id
):
return contract_declarer.state_variables_by_ref_id[referenced_declaration]
# Ccontracts ids are the referenced declaration
# This is not true for the functions, as we dont always have the referenced_declaration
# But maybe we could? (TODO)
for contract_candidate in all_contracts:
@ -72,14 +88,9 @@ def _find_variable_from_ref_declaration(
def _find_variable_in_function_parser(
var_name: str,
function_parser: Optional["FunctionSolc"],
referenced_declaration: Optional[int] = None,
) -> Optional[Variable]:
if function_parser is None:
return None
# We look for variable declared with the referencedDeclaration attr
func_variables_renamed = function_parser.variables_renamed
if referenced_declaration and referenced_declaration in func_variables_renamed:
return func_variables_renamed[referenced_declaration].underlying_variable
# If not found, check for name
func_variables = function_parser.underlying_function.variables_as_dict
if var_name in func_variables:
@ -114,6 +125,8 @@ def find_top_level(
:return:
:rtype:
"""
if var_name in scope.type_aliases:
return scope.type_aliases[var_name], False
if var_name in scope.structures:
return scope.structures[var_name], False
@ -205,6 +218,10 @@ def _find_in_contract(
if sig == var_name:
return modifier
type_aliases = contract.type_aliases_as_dict
if var_name in type_aliases:
return type_aliases[var_name]
# structures are looked on the contract declarer
structures = contract.structures_as_dict
if var_name in structures:
@ -240,6 +257,7 @@ def _find_in_contract(
return None
# pylint: disable=too-many-statements
def _find_variable_init(
caller_context: CallerContextExpression,
) -> Tuple[List[Contract], List["Function"], FileScope,]:
@ -247,6 +265,7 @@ def _find_variable_init(
from slither.solc_parsing.declarations.function import FunctionSolc
from slither.solc_parsing.declarations.structure_top_level import StructureTopLevelSolc
from slither.solc_parsing.variables.top_level_variable import TopLevelVariableSolc
from slither.solc_parsing.declarations.custom_error import CustomErrorSolc
direct_contracts: List[Contract]
direct_functions_parser: List[Function]
@ -289,6 +308,24 @@ def _find_variable_init(
direct_contracts = []
direct_functions_parser = []
scope = caller_context.underlying_variable.file_scope
elif isinstance(caller_context, CustomErrorSolc):
if caller_context.contract_parser:
direct_contracts = [caller_context.contract_parser.underlying_contract]
direct_functions_parser = [
f.underlying_function
for f in caller_context.contract_parser.functions_parser
+ caller_context.contract_parser.modifiers_parser
]
else:
# Top level custom error
direct_contracts = []
direct_functions_parser = []
underlying_custom_error = caller_context.underlying_custom_error
if isinstance(underlying_custom_error, CustomErrorTopLevel):
scope = underlying_custom_error.file_scope
else:
assert isinstance(underlying_custom_error, CustomErrorContract)
scope = underlying_custom_error.contract.file_scope
else:
raise SlitherError(
f"{type(caller_context)} ({caller_context} is not valid for find_variable"
@ -337,6 +374,7 @@ def find_variable(
"""
from slither.solc_parsing.declarations.function import FunctionSolc
from slither.solc_parsing.declarations.contract import ContractSolc
from slither.solc_parsing.declarations.custom_error import CustomErrorSolc
# variable are looked from the contract declarer
# functions can be shadowed, but are looked from the contract instance, rather than the contract declarer
@ -362,23 +400,6 @@ def find_variable(
if var_name in current_scope.renaming:
var_name = current_scope.renaming[var_name]
if var_name in current_scope.user_defined_types:
return current_scope.user_defined_types[var_name], False
# Use ret0/ret1 to help mypy
ret0 = _find_variable_from_ref_declaration(
referenced_declaration, direct_contracts, direct_functions
)
if ret0:
return ret0, False
function_parser: Optional[FunctionSolc] = (
caller_context if isinstance(caller_context, FunctionSolc) else None
)
ret1 = _find_variable_in_function_parser(var_name, function_parser, referenced_declaration)
if ret1:
return ret1, False
contract: Optional[Contract] = None
contract_declarer: Optional[Contract] = None
if isinstance(caller_context, ContractSolc):
@ -391,6 +412,33 @@ def find_variable(
contract_declarer = underlying_func.contract_declarer
else:
assert isinstance(underlying_func, FunctionTopLevel)
elif isinstance(caller_context, CustomErrorSolc):
underlying_custom_error = caller_context.underlying_custom_error
if isinstance(underlying_custom_error, CustomErrorContract):
contract = underlying_custom_error.contract
# We check for contract variables here because _find_in_contract
# will return since in this case the contract_declarer is None
for var in contract.variables:
if var_name == var.name:
return var, False
function_parser: Optional[FunctionSolc] = (
caller_context if isinstance(caller_context, FunctionSolc) else None
)
# Use ret0/ret1 to help mypy
ret0 = _find_variable_from_ref_declaration(
referenced_declaration,
direct_contracts,
direct_functions,
function_parser,
contract_declarer,
)
if ret0:
return ret0, False
ret1 = _find_variable_in_function_parser(var_name, function_parser)
if ret1:
return ret1, False
ret = _find_in_contract(var_name, contract, contract_declarer, is_super, is_identifier_path)
if ret:
@ -442,6 +490,8 @@ def find_variable(
referenced_declaration,
list(current_scope.contracts.values()),
list(current_scope.functions),
None,
None,
)
if ret:
return ret, False

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

@ -82,9 +82,9 @@ def _find_from_type_name( # pylint: disable=too-many-locals,too-many-branches,t
# all_enums = [c.enums for c in contracts]
# all_enums = [item for sublist in all_enums for item in sublist]
# all_enums += contract.slither.enums_top_level
var_type = next((e for e in all_enums if e.name == enum_name), None)
var_type = next((e for e in all_enums if e.canonical_name == enum_name), None)
if not var_type:
var_type = next((e for e in all_enums if e.canonical_name == enum_name), None)
var_type = next((e for e in all_enums if e.name == enum_name), None)
if not var_type:
# any contract can refer to another contract's structure
name_struct = name
@ -94,9 +94,9 @@ def _find_from_type_name( # pylint: disable=too-many-locals,too-many-branches,t
# all_structures = [c.structures for c in contracts]
# all_structures = [item for sublist in all_structures for item in sublist]
# all_structures += contract.slither.structures_top_level
var_type = next((st for st in all_structures if st.name == name_struct), None)
var_type = next((st for st in all_structures if st.canonical_name == name_struct), None)
if not var_type:
var_type = next((st for st in all_structures if st.canonical_name == name_struct), None)
var_type = next((st for st in all_structures if st.name == name_struct), None)
# case where struct xxx.xx[] where not well formed in the AST
if not var_type:
depth = 0
@ -235,7 +235,7 @@ def parse_type(
sl: "SlitherCompilationUnit"
renaming: Dict[str, str]
user_defined_types: Dict[str, TypeAlias]
type_aliases: Dict[str, TypeAlias]
enums_direct_access: List["Enum"] = []
# Note: for convenicence top level functions use the same parser than function in contract
# but contract_parser is set to None
@ -247,13 +247,13 @@ def parse_type(
sl = caller_context.compilation_unit
next_context = caller_context
renaming = {}
user_defined_types = sl.user_defined_value_types
type_aliases = sl.type_aliases
else:
assert isinstance(caller_context, FunctionSolc)
sl = caller_context.underlying_function.compilation_unit
next_context = caller_context.slither_parser
renaming = caller_context.underlying_function.file_scope.renaming
user_defined_types = caller_context.underlying_function.file_scope.user_defined_types
type_aliases = caller_context.underlying_function.file_scope.type_aliases
structures_direct_access = sl.structures_top_level
all_structuress = [c.structures for c in sl.contracts]
all_structures = [item for sublist in all_structuress for item in sublist]
@ -299,7 +299,7 @@ def parse_type(
functions = list(scope.functions)
renaming = scope.renaming
user_defined_types = scope.user_defined_types
type_aliases = scope.type_aliases
elif isinstance(caller_context, (ContractSolc, FunctionSolc)):
sl = caller_context.compilation_unit
if isinstance(caller_context, FunctionSolc):
@ -329,7 +329,7 @@ def parse_type(
functions = contract.functions + contract.modifiers
renaming = scope.renaming
user_defined_types = scope.user_defined_types
type_aliases = scope.type_aliases
else:
raise ParsingError(f"Incorrect caller context: {type(caller_context)}")
@ -343,8 +343,8 @@ def parse_type(
name = t.name
if name in renaming:
name = renaming[name]
if name in user_defined_types:
return user_defined_types[name]
if name in type_aliases:
return type_aliases[name]
return _find_from_type_name(
name,
functions,
@ -365,9 +365,9 @@ def parse_type(
name = t["typeDescriptions"]["typeString"]
if name in renaming:
name = renaming[name]
if name in user_defined_types:
_add_type_references(user_defined_types[name], t["src"], sl)
return user_defined_types[name]
if name in type_aliases:
_add_type_references(type_aliases[name], t["src"], sl)
return type_aliases[name]
type_found = _find_from_type_name(
name,
functions,
@ -386,9 +386,9 @@ def parse_type(
name = t["attributes"][type_name_key]
if name in renaming:
name = renaming[name]
if name in user_defined_types:
_add_type_references(user_defined_types[name], t["src"], sl)
return user_defined_types[name]
if name in type_aliases:
_add_type_references(type_aliases[name], t["src"], sl)
return type_aliases[name]
type_found = _find_from_type_name(
name,
functions,
@ -407,8 +407,8 @@ def parse_type(
name = t["name"]
if name in renaming:
name = renaming[name]
if name in user_defined_types:
return user_defined_types[name]
if name in type_aliases:
return type_aliases[name]
type_found = _find_from_type_name(
name,
functions,

@ -748,7 +748,7 @@ def parse_yul_function_call(root: YulScope, node: YulNode, ast: Dict) -> Optiona
def _check_for_state_variable_name(root: YulScope, potential_name: str) -> Optional[Identifier]:
root_function = root.function
if isinstance(root_function, FunctionContract):
var = root_function.contract.get_state_variable_from_name(potential_name)
var = root_function.contract_declarer.get_state_variable_from_name(potential_name)
if var:
return Identifier(var)
return None

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

@ -12,16 +12,18 @@ from slither.core.solidity_types import (
MappingType,
ArrayType,
ElementaryType,
TypeAlias,
)
from slither.core.declarations import Structure, Enum, Contract
from slither.core.declarations import Structure, StructureContract, Enum, Contract
if TYPE_CHECKING:
from slither.core.declarations import FunctionContract, CustomErrorContract
from slither.core.variables.state_variable import StateVariable
from slither.core.variables.local_variable import LocalVariable
from slither.core.variables.structure_variable import StructureVariable
# pylint: disable=too-many-arguments
# pylint: disable=too-many-arguments,too-many-locals,too-many-branches
def generate_interface(
contract: "Contract",
unroll_structs: bool = True,
@ -56,12 +58,47 @@ def generate_interface(
for enum in contract.enums:
interface += f" enum {enum.name} {{ {', '.join(enum.values)} }}\n"
if include_structs:
for struct in contract.structures:
# Include structures defined in this contract and at the top level
structs = contract.structures + contract.compilation_unit.structures_top_level
# Function signatures may reference other structures as well
# Include structures defined in libraries used for them
for _for in contract.using_for.keys():
if (
isinstance(_for, UserDefinedType)
and isinstance(_for.type, StructureContract)
and _for.type not in structs
):
structs.append(_for.type)
# Include any other structures used as function arguments/returns
for func in contract.functions_entry_points:
for arg in func.parameters + func.returns:
_type = arg.type
if isinstance(_type, ArrayType):
_type = _type.type
while isinstance(_type, MappingType):
_type = _type.type_to
if isinstance(_type, UserDefinedType):
_type = _type.type
if isinstance(_type, Structure) and _type not in structs:
structs.append(_type)
for struct in structs:
interface += generate_struct_interface_str(struct, indent=4)
for elem in struct.elems_ordered:
if (
isinstance(elem.type, UserDefinedType)
and isinstance(elem.type.type, StructureContract)
and elem.type.type not in structs
):
structs.append(elem.type.type)
for var in contract.state_variables_entry_points:
interface += f" function {generate_interface_variable_signature(var, unroll_structs)};\n"
# if any(func.name == var.name for func in contract.functions_entry_points):
# # ignore public variables that override a public function
# continue
var_sig = generate_interface_variable_signature(var, unroll_structs)
if var_sig is not None and var_sig != "":
interface += f" function {var_sig};\n"
for func in contract.functions_entry_points:
if func.is_constructor or func.is_fallback or func.is_receive:
if func.is_constructor or func.is_fallback or func.is_receive or not func.is_implemented:
continue
interface += (
f" function {generate_interface_function_signature(func, unroll_structs)};\n"
@ -75,6 +112,10 @@ def generate_interface_variable_signature(
) -> Optional[str]:
if var.visibility in ["private", "internal"]:
return None
if isinstance(var.type, UserDefinedType) and isinstance(var.type.type, Structure):
for elem in var.type.type.elems_ordered:
if isinstance(elem.type, MappingType):
return ""
if unroll_structs:
params = [
convert_type_for_solidity_signature_to_string(x).replace("(", "").replace(")", "")
@ -93,6 +134,11 @@ def generate_interface_variable_signature(
_type = _type.type_to
while isinstance(_type, (ArrayType, UserDefinedType)):
_type = _type.type
if isinstance(_type, TypeAlias):
_type = _type.type
if isinstance(_type, Structure):
if any(isinstance(elem.type, MappingType) for elem in _type.elems_ordered):
return ""
ret = str(_type)
if isinstance(_type, Structure) or (isinstance(_type, Type) and _type.is_dynamic):
ret += " memory"
@ -125,6 +171,8 @@ def generate_interface_function_signature(
.replace("(", "")
.replace(")", "")
)
if var.type.is_dynamic:
return f"{_handle_dynamic_struct_elem(var.type)} {var.location}"
if isinstance(var.type, ArrayType) and isinstance(
var.type.type, (UserDefinedType, ElementaryType)
):
@ -135,12 +183,14 @@ def generate_interface_function_signature(
+ f" {var.location}"
)
if isinstance(var.type, UserDefinedType):
if isinstance(var.type.type, (Structure, Enum)):
if isinstance(var.type.type, Structure):
return f"{str(var.type.type)} memory"
if isinstance(var.type.type, Enum):
return str(var.type.type)
if isinstance(var.type.type, Contract):
return "address"
if var.type.is_dynamic:
return f"{var.type} {var.location}"
if isinstance(var.type, TypeAlias):
return str(var.type.type)
return str(var.type)
name, _, _ = func.signature
@ -154,6 +204,12 @@ def generate_interface_function_signature(
view = " view" if func.view and not func.pure else ""
pure = " pure" if func.pure else ""
payable = " payable" if func.payable else ""
# Make sure the function doesn't return a struct with nested mappings
for ret in func.returns:
if isinstance(ret.type, UserDefinedType) and isinstance(ret.type.type, Structure):
for elem in ret.type.type.elems_ordered:
if isinstance(elem.type, MappingType):
return ""
returns = [format_var(ret, unroll_structs) for ret in func.returns]
parameters = [format_var(param, unroll_structs) for param in func.parameters]
_interface_signature_str = (
@ -184,17 +240,49 @@ def generate_struct_interface_str(struct: "Structure", indent: int = 0) -> str:
spaces += " "
definition = f"{spaces}struct {struct.name} {{\n"
for elem in struct.elems_ordered:
if isinstance(elem.type, UserDefinedType):
if isinstance(elem.type.type, (Structure, Enum)):
if elem.type.is_dynamic:
definition += f"{spaces} {_handle_dynamic_struct_elem(elem.type)} {elem.name};\n"
elif isinstance(elem.type, UserDefinedType):
if isinstance(elem.type.type, Structure):
definition += f"{spaces} {elem.type.type} {elem.name};\n"
elif isinstance(elem.type.type, Contract):
definition += f"{spaces} address {elem.name};\n"
else:
definition += f"{spaces} {convert_type_for_solidity_signature_to_string(elem.type)} {elem.name};\n"
elif isinstance(elem.type, TypeAlias):
definition += f"{spaces} {elem.type.type} {elem.name};\n"
else:
definition += f"{spaces} {elem.type} {elem.name};\n"
definition += f"{spaces}}}\n"
return definition
def _handle_dynamic_struct_elem(elem_type: Type) -> str:
assert elem_type.is_dynamic
if isinstance(elem_type, ElementaryType):
return f"{elem_type}"
if isinstance(elem_type, ArrayType):
base_type = elem_type.type
if isinstance(base_type, UserDefinedType):
if isinstance(base_type.type, Contract):
return "address[]"
if isinstance(base_type.type, Enum):
return convert_type_for_solidity_signature_to_string(elem_type)
return f"{base_type.type.name}[]"
return f"{base_type}[]"
if isinstance(elem_type, MappingType):
type_to = elem_type.type_to
type_from = elem_type.type_from
if isinstance(type_from, UserDefinedType) and isinstance(type_from.type, Contract):
type_from = ElementaryType("address")
if isinstance(type_to, MappingType):
return f"mapping({type_from} => {_handle_dynamic_struct_elem(type_to)})"
if isinstance(type_to, UserDefinedType):
if isinstance(type_to.type, Contract):
return f"mapping({type_from} => address)"
return f"mapping({type_from} => {type_to.type.name})"
return f"{elem_type}"
return ""
def generate_custom_error_interface(
error: "CustomErrorContract", unroll_structs: bool = True
) -> str:

@ -68,6 +68,8 @@ defaults_flag_in_config = {
"zip_type": "lzma",
"show_ignored_findings": False,
"no_fail": False,
"sarif_input": "export.sarif",
"sarif_triage": "export.sarif.sarifexplorer",
**DEFAULTS_FLAG_IN_CONFIG_CRYTIC_COMPILE,
}

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

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

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

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

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

@ -18,6 +18,7 @@ from slither.core.declarations import (
Structure,
Pragma,
FunctionContract,
CustomError,
)
from slither.core.source_mapping.source_mapping import SourceMapping
from slither.core.variables.local_variable import LocalVariable
@ -438,6 +439,8 @@ class Output:
self.add_event(add, additional_fields=additional_fields)
elif isinstance(add, Structure):
self.add_struct(add, additional_fields=additional_fields)
elif isinstance(add, CustomError):
self.add_custom_error(add, additional_fields=additional_fields)
elif isinstance(add, Pragma):
self.add_pragma(add, additional_fields=additional_fields)
elif isinstance(add, Node):
@ -585,6 +588,32 @@ class Output:
self._data["elements"].append(element)
# endregion
###################################################################################
###################################################################################
# region CustomError
###################################################################################
###################################################################################
def add_custom_error(
self, custom_error: CustomError, additional_fields: Optional[Dict] = None
) -> None:
if additional_fields is None:
additional_fields = {}
type_specific_fields = {
"parent": _create_parent_element(custom_error),
"signature": custom_error.full_name,
}
element = _create_base_element(
"custom_error",
custom_error.name,
custom_error.source_mapping.to_json(),
type_specific_fields,
additional_fields,
)
self._data["elements"].append(element)
# endregion
###################################################################################
###################################################################################

@ -0,0 +1,71 @@
"""
Various utils for sarif/vscode
"""
import json
from pathlib import Path
from typing import List, Dict, Optional, Tuple, Any
def _parse_index(key: str) -> Optional[Tuple[int, int]]:
if key.count(":") != 2:
return None
try:
run = int(key[key.find(":") + 1 : key.rfind(":")])
index = int(key[key.rfind(":") + 1 :])
return run, index
except ValueError:
return None
def _get_indexes(path_to_triage: Path) -> List[Tuple[int, int]]:
try:
with open(path_to_triage, encoding="utf8") as file_desc:
triage = json.load(file_desc)
except json.decoder.JSONDecodeError:
return []
resultIdToNotes: Dict[str, Dict] = triage.get("resultIdToNotes", {})
indexes: List[Tuple[int, int]] = []
for key, data in resultIdToNotes.items():
if "status" in data and data["status"] == 1:
parsed = _parse_index(key)
if parsed:
indexes.append(parsed)
return indexes
def read_triage_info(path_to_sarif: Path, path_to_triage: Path) -> List[str]:
try:
with open(path_to_sarif, encoding="utf8") as file_desc:
sarif = json.load(file_desc)
except json.decoder.JSONDecodeError:
return []
runs: List[Dict[str, Any]] = sarif.get("runs", [])
# Don't support multiple runs for now
if len(runs) != 1:
return []
run_results: List[Dict] = runs[0].get("results", [])
indexes = _get_indexes(path_to_triage)
ids: List[str] = []
for run, index in indexes:
# We dont support multiple runs for now
if run != 0:
continue
try:
elem = run_results[index]
except KeyError:
continue
if "partialFingerprints" in elem:
if "id" in elem["partialFingerprints"]:
ids.append(elem["partialFingerprints"]["id"])
return ids

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

@ -2,6 +2,7 @@ import logging
from typing import Union, List, TYPE_CHECKING, Any
from slither.core import expressions
from slither.core.scope.scope import FileScope
from slither.core.declarations import (
Function,
SolidityVariable,
@ -11,6 +12,9 @@ from slither.core.declarations import (
EnumContract,
EnumTopLevel,
Enum,
SolidityImportPlaceHolder,
Import,
Structure,
)
from slither.core.expressions import (
AssignmentOperation,
@ -50,6 +54,7 @@ from slither.slithir.operations import (
Member,
TypeConversion,
Unary,
UnaryType,
Unpack,
Return,
SolidityCall,
@ -109,6 +114,13 @@ _binary_to_binary = {
BinaryOperationType.OROR: BinaryType.OROR,
}
_unary_to_unary = {
UnaryOperationType.BANG: UnaryType.BANG,
UnaryOperationType.TILD: UnaryType.TILD,
}
_signed_to_unsigned = {
BinaryOperationType.DIVISION_SIGNED: BinaryType.DIVISION,
BinaryOperationType.MODULO_SIGNED: BinaryType.MODULO,
@ -170,6 +182,7 @@ class ExpressionToSlithIR(ExpressionVisitor):
def result(self) -> List[Operation]:
return self._result
# pylint: disable=too-many-branches,too-many-statements
def _post_assignement_operation(self, expression: AssignmentOperation) -> None:
left = get(expression.expression_left)
right = get(expression.expression_right)
@ -233,6 +246,35 @@ class ExpressionToSlithIR(ExpressionVisitor):
operation.set_expression(expression)
self._result.append(operation)
set_val(expression, left)
elif (
isinstance(left.type, UserDefinedType)
and isinstance(left.type.type, Structure)
and isinstance(right, TupleVariable)
):
# This will result in a `NewStructure` operation where
# each field is assigned the value unpacked from the tuple
# (see `slither.vyper_parsing.type_parsing.parse_type`)
args = []
for idx, elem in enumerate(left.type.type.elems.values()):
temp = TemporaryVariable(self._node)
temp.type = elem.type
args.append(temp)
operation = Unpack(temp, right, idx)
operation.set_expression(expression)
self._result.append(operation)
for arg in args:
op = Argument(arg)
op.set_expression(expression)
self._result.append(op)
operation = TmpCall(
left.type.type, len(left.type.type.elems), left, left.type.type.name
)
operation.set_expression(expression)
self._result.append(operation)
else:
operation = convert_assignment(
left, right, expression.type, expression.expression_return_type
@ -304,7 +346,9 @@ class ExpressionToSlithIR(ExpressionVisitor):
val = TupleVariable(self._node)
else:
val = TemporaryVariable(self._node)
internal_call = InternalCall(called, len(args), val, expression.type_call)
internal_call = InternalCall(
called, len(args), val, expression.type_call, names=expression.names
)
internal_call.set_expression(expression)
self._result.append(internal_call)
set_val(expression, val)
@ -373,7 +417,9 @@ class ExpressionToSlithIR(ExpressionVisitor):
else:
val = TemporaryVariable(self._node)
message_call = TmpCall(called, len(args), val, expression.type_call)
message_call = TmpCall(
called, len(args), val, expression.type_call, names=expression.names
)
message_call.set_expression(expression)
# Gas/value are only accessible here if the syntax {gas: , value: }
# Is used over .gas().value()
@ -417,6 +463,21 @@ class ExpressionToSlithIR(ExpressionVisitor):
set_val(expression, t)
return
val = ReferenceVariable(self._node)
if (
isinstance(left, LocalVariable)
and isinstance(left.type, UserDefinedType)
and isinstance(left.type.type, Structure)
):
# We rewrite the index access to a tuple variable as
# an access to its field i.e. the 0th element is the field "_0"
# (see `slither.vyper_parsing.type_parsing.parse_type`)
operation = Member(left, Constant("_" + str(right)), val)
operation.set_expression(expression)
self._result.append(operation)
set_val(expression, val)
return
# access to anonymous array
# such as [0,1][x]
if isinstance(left, list):
@ -426,6 +487,7 @@ class ExpressionToSlithIR(ExpressionVisitor):
operation = InitArray(init_array_right, init_array_val)
operation.set_expression(expression)
self._result.append(operation)
operation = Index(val, left, right)
operation.set_expression(expression)
self._result.append(operation)
@ -443,6 +505,7 @@ class ExpressionToSlithIR(ExpressionVisitor):
# Look for type(X).max / min
# Because we looked at the AST structure, we need to look into the nested expression
# Hopefully this is always on a direct sub field, and there is no weird construction
# pylint: disable=too-many-nested-blocks
if isinstance(expression.expression, CallExpression) and expression.member_name in [
"min",
"max",
@ -462,10 +525,22 @@ class ExpressionToSlithIR(ExpressionVisitor):
constant_type = type_found
else:
# type(enum).max/min
assert isinstance(type_expression_found, Identifier)
type_found_in_expression = type_expression_found.value
assert isinstance(type_found_in_expression, (EnumContract, EnumTopLevel))
type_found = UserDefinedType(type_found_in_expression)
# Case when enum is in another contract e.g. type(C.E).max
if isinstance(type_expression_found, MemberAccess):
contract = type_expression_found.expression.value
assert isinstance(contract, Contract)
for enum in contract.enums:
if enum.name == type_expression_found.member_name:
type_found_in_expression = enum
type_found = UserDefinedType(enum)
break
else:
assert isinstance(type_expression_found, Identifier)
type_found_in_expression = type_expression_found.value
assert isinstance(
type_found_in_expression, (EnumContract, EnumTopLevel)
)
type_found = UserDefinedType(type_found_in_expression)
constant_type = None
min_value = type_found_in_expression.min
max_value = type_found_in_expression.max
@ -516,13 +591,26 @@ class ExpressionToSlithIR(ExpressionVisitor):
# contract A { type MyInt is int}
# contract B { function f() public{ A.MyInt test = A.MyInt.wrap(1);}}
# The logic is handled by _post_call_expression
if expression.member_name in expr.file_scope.user_defined_types:
set_val(expression, expr.file_scope.user_defined_types[expression.member_name])
if expression.member_name in expr.file_scope.type_aliases:
set_val(expression, expr.file_scope.type_aliases[expression.member_name])
return
# Lookup errors referred to as member of contract e.g. Test.myError.selector
if expression.member_name in expr.custom_errors_as_dict:
set_val(expression, expr.custom_errors_as_dict[expression.member_name])
return
# Lookup enums when in a different contract e.g. C.E
if str(expression) in expr.enums_as_dict:
set_val(expression, expr.enums_as_dict[str(expression)])
return
if isinstance(expr, (SolidityImportPlaceHolder, Import)):
scope = (
expr.import_directive.scope
if isinstance(expr, SolidityImportPlaceHolder)
else expr.scope
)
if self._check_elem_in_scope(expression.member_name, scope, expression):
return
val_ref = ReferenceVariable(self._node)
member = Member(expr, Constant(expression.member_name), val_ref)
@ -530,6 +618,61 @@ class ExpressionToSlithIR(ExpressionVisitor):
self._result.append(member)
set_val(expression, val_ref)
def _check_elem_in_scope(self, elem: str, scope: FileScope, expression: MemberAccess) -> bool:
if elem in scope.renaming:
self._check_elem_in_scope(scope.renaming[elem], scope, expression)
return True
if elem in scope.contracts:
set_val(expression, scope.contracts[elem])
return True
if elem in scope.structures:
set_val(expression, scope.structures[elem])
return True
if elem in scope.variables:
set_val(expression, scope.variables[elem])
return True
if elem in scope.enums:
set_val(expression, scope.enums[elem])
return True
if elem in scope.type_aliases:
set_val(expression, scope.type_aliases[elem])
return True
for import_directive in scope.imports:
if elem == import_directive.alias:
set_val(expression, import_directive)
return True
for custom_error in scope.custom_errors:
if custom_error.name == elem:
set_val(expression, custom_error)
return True
if str(expression.type).startswith("function "):
# This is needed to handle functions overloading
signature_to_seaarch = (
str(expression.type)
.replace("function ", elem)
.replace("pure ", "")
.replace("view ", "")
.replace("struct ", "")
.replace("enum ", "")
.replace(" memory", "")
.split(" returns", maxsplit=1)[0]
)
for function in scope.functions:
if signature_to_seaarch == function.full_name:
set_val(expression, function)
return True
return False
def _post_new_array(self, expression: NewArray) -> None:
val = TemporaryVariable(self._node)
operation = TmpNewArray(expression.array_type, val)
@ -572,7 +715,7 @@ class ExpressionToSlithIR(ExpressionVisitor):
expr = get(expression.expression)
val = TemporaryVariable(self._node)
expression_type = expression.type
assert isinstance(expression_type, (TypeAlias, UserDefinedType, ElementaryType))
assert isinstance(expression_type, (TypeAlias, UserDefinedType, ElementaryType, ArrayType))
operation = TypeConversion(val, expr, expression_type)
val.set_type(expression.type)
operation.set_expression(expression)
@ -585,7 +728,7 @@ class ExpressionToSlithIR(ExpressionVisitor):
operation: Operation
if expression.type in [UnaryOperationType.BANG, UnaryOperationType.TILD]:
lvalue = TemporaryVariable(self._node)
operation = Unary(lvalue, value, expression.type)
operation = Unary(lvalue, value, _unary_to_unary[expression.type])
operation.set_expression(expression)
self._result.append(operation)
set_val(expression, lvalue)

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

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

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

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

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

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

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

Loading…
Cancel
Save