Merge remote-tracking branch 'crytic/slither/dev' into slither/dev-upgradeability-complex-datatype

pull/1535/head
webthethird 2 years ago
commit 19b3af3449
  1. 9
      .github/workflows/IR.yml
  2. 7
      .github/workflows/ci.yml
  3. 3
      .github/workflows/detectors.yml
  4. 87
      .github/workflows/doctor.yml
  5. 3
      .github/workflows/features.yml
  6. 2
      .github/workflows/pip-audit.yml
  7. 6
      CODEOWNERS
  8. 9
      CONTRIBUTING.md
  9. 5
      Dockerfile
  10. 17
      scripts/ci_test_etherscan.sh
  11. 6
      setup.py
  12. 45
      slither/__main__.py
  13. 4
      slither/core/children/child_function.py
  14. 6
      slither/core/compilation_unit.py
  15. 3
      slither/core/declarations/__init__.py
  16. 31
      slither/core/declarations/contract.py
  17. 3
      slither/core/declarations/function.py
  18. 18
      slither/core/declarations/using_for_top_level.py
  19. 4
      slither/core/expressions/identifier.py
  20. 21
      slither/core/expressions/literal.py
  21. 121
      slither/core/scope/scope.py
  22. 4
      slither/core/slither_core.py
  23. 4
      slither/core/variables/local_variable.py
  24. 3
      slither/detectors/all_detectors.py
  25. 2
      slither/detectors/attributes/constant_pragma.py
  26. 45
      slither/detectors/variables/could_be_constant.py
  27. 44
      slither/detectors/variables/could_be_immutable.py
  28. 125
      slither/detectors/variables/possible_const_state_variables.py
  29. 125
      slither/detectors/variables/unchanged_state_variables.py
  30. 6
      slither/formatters/variables/unchanged_state_variables.py
  31. 11
      slither/printers/guidance/echidna.py
  32. 18
      slither/printers/summary/evm.py
  33. 44
      slither/slither.py
  34. 131
      slither/slithir/convert.py
  35. 80
      slither/solc_parsing/declarations/contract.py
  36. 8
      slither/solc_parsing/declarations/function.py
  37. 2
      slither/solc_parsing/declarations/modifier.py
  38. 167
      slither/solc_parsing/declarations/using_for_top_level.py
  39. 71
      slither/solc_parsing/slither_compilation_unit_solc.py
  40. 8
      slither/solc_parsing/solidity_types/type_parsing.py
  41. 4
      slither/solc_parsing/yul/evm_functions.py
  42. 64
      slither/solc_parsing/yul/parse_yul.py
  43. 5
      slither/tools/doctor/__main__.py
  44. 2
      slither/tools/doctor/checks/__init__.py
  45. 85
      slither/tools/doctor/checks/paths.py
  46. 10
      slither/tools/doctor/checks/versions.py
  47. 5
      slither/tools/documentation/README.md
  48. 0
      slither/tools/documentation/__init__.py
  49. 280
      slither/tools/documentation/__main__.py
  50. 6
      slither/tools/slither_format/slither_format.py
  51. 109
      slither/tools/upgradeability/__main__.py
  52. 4
      slither/tools/upgradeability/checks/variable_initialization.py
  53. 36
      slither/tools/upgradeability/checks/variables_order.py
  54. 60
      slither/utils/codex.py
  55. 1
      slither/utils/command_line.py
  56. 174
      slither/utils/expression_manipulations.py
  57. 76
      slither/visitors/expression/constants_folding.py
  58. 15
      slither/visitors/slithir/expression_to_slithir.py
  59. BIN
      tests/ast-parsing/compile/complex_imports/import_aliases_issue_1319/test.sol-0.5.12-compact.zip
  60. BIN
      tests/ast-parsing/compile/complex_imports/import_aliases_issue_1319/test.sol-0.5.12-legacy.zip
  61. BIN
      tests/ast-parsing/compile/custom-error-selector.sol-0.8.10-compact.zip
  62. BIN
      tests/ast-parsing/compile/custom-error-selector.sol-0.8.11-compact.zip
  63. BIN
      tests/ast-parsing/compile/custom-error-selector.sol-0.8.12-compact.zip
  64. BIN
      tests/ast-parsing/compile/custom-error-selector.sol-0.8.13-compact.zip
  65. BIN
      tests/ast-parsing/compile/custom-error-selector.sol-0.8.14-compact.zip
  66. BIN
      tests/ast-parsing/compile/custom-error-selector.sol-0.8.15-compact.zip
  67. BIN
      tests/ast-parsing/compile/custom-error-selector.sol-0.8.4-compact.zip
  68. BIN
      tests/ast-parsing/compile/custom-error-selector.sol-0.8.5-compact.zip
  69. BIN
      tests/ast-parsing/compile/custom-error-selector.sol-0.8.6-compact.zip
  70. BIN
      tests/ast-parsing/compile/custom-error-selector.sol-0.8.7-compact.zip
  71. BIN
      tests/ast-parsing/compile/custom-error-selector.sol-0.8.8-compact.zip
  72. BIN
      tests/ast-parsing/compile/custom-error-selector.sol-0.8.9-compact.zip
  73. BIN
      tests/ast-parsing/compile/top-level-struct-0.8.0.sol-0.8.0-compact.zip
  74. BIN
      tests/ast-parsing/compile/user_defined_value_type/using-for-0.8.8.sol-0.8.10-compact.zip
  75. BIN
      tests/ast-parsing/compile/user_defined_value_type/using-for-0.8.8.sol-0.8.11-compact.zip
  76. BIN
      tests/ast-parsing/compile/user_defined_value_type/using-for-0.8.8.sol-0.8.12-compact.zip
  77. BIN
      tests/ast-parsing/compile/user_defined_value_type/using-for-0.8.8.sol-0.8.13-compact.zip
  78. BIN
      tests/ast-parsing/compile/user_defined_value_type/using-for-0.8.8.sol-0.8.14-compact.zip
  79. BIN
      tests/ast-parsing/compile/user_defined_value_type/using-for-0.8.8.sol-0.8.15-compact.zip
  80. BIN
      tests/ast-parsing/compile/user_defined_value_type/using-for-0.8.8.sol-0.8.8-compact.zip
  81. BIN
      tests/ast-parsing/compile/using-for-1-0.8.0.sol-0.8.15-compact.zip
  82. BIN
      tests/ast-parsing/compile/using-for-2-0.8.0.sol-0.8.15-compact.zip
  83. BIN
      tests/ast-parsing/compile/using-for-3-0.8.0.sol-0.8.15-compact.zip
  84. BIN
      tests/ast-parsing/compile/using-for-4-0.8.0.sol-0.8.15-compact.zip
  85. BIN
      tests/ast-parsing/compile/using-for-alias-contract-0.8.0.sol-0.8.15-compact.zip
  86. BIN
      tests/ast-parsing/compile/using-for-alias-top-level-0.8.0.sol-0.8.15-compact.zip
  87. BIN
      tests/ast-parsing/compile/using-for-functions-list-1-0.8.0.sol-0.8.15-compact.zip
  88. BIN
      tests/ast-parsing/compile/using-for-functions-list-2-0.8.0.sol-0.8.15-compact.zip
  89. BIN
      tests/ast-parsing/compile/using-for-functions-list-3-0.8.0.sol-0.8.15-compact.zip
  90. BIN
      tests/ast-parsing/compile/using-for-functions-list-4-0.8.0.sol-0.8.15-compact.zip
  91. BIN
      tests/ast-parsing/compile/using-for-global-0.8.0.sol-0.8.15-compact.zip
  92. BIN
      tests/ast-parsing/compile/using-for-in-library-0.8.0.sol-0.8.15-compact.zip
  93. BIN
      tests/ast-parsing/compile/yul-top-level-0.8.0.sol-0.8.0-compact.zip
  94. 1
      tests/ast-parsing/complex_imports/import_aliases_issue_1319/import.sol
  95. 9
      tests/ast-parsing/complex_imports/import_aliases_issue_1319/test.sol
  96. 9
      tests/ast-parsing/complex_imports/import_aliases_issue_1319/test_fail.sol
  97. 13
      tests/ast-parsing/custom-error-selector.sol
  98. 6
      tests/ast-parsing/expected/complex_imports/import_aliases_issue_1319/test.sol-0.5.12-compact.json
  99. 6
      tests/ast-parsing/expected/complex_imports/import_aliases_issue_1319/test.sol-0.5.12-legacy.json
  100. 10
      tests/ast-parsing/expected/custom-error-selector.sol-0.8.10-compact.json
  101. Some files were not shown because too many files have changed in this diff Show More

@ -34,8 +34,13 @@ jobs:
- name: Install dependencies
run: |
pip install ".[dev]"
solc-select install all
solc-select use 0.8.11
solc-select install 0.5.0
solc-select use 0.8.11 --always-install
- name: Install old solc
if: matrix.os == 'ubuntu-latest'
run: solc-select install 0.4.0
- name: Test with pytest
run: |

@ -29,7 +29,7 @@ jobs:
# "embark",
"erc",
# "etherlime",
# "etherscan"
"etherscan",
"find_paths",
"flat",
"kspec",
@ -55,8 +55,9 @@ jobs:
- name: Install dependencies
run: |
pip install ".[dev]"
solc-select install all
solc-select use 0.5.1
solc-select use 0.4.25 --always-install
solc-select use 0.8.0 --always-install
solc-select use 0.5.1 --always-install
pip install typing_extensions==4.1.1
pip install importlib_metadata==4.8.3

@ -35,8 +35,7 @@ jobs:
run: |
pip install ".[dev]"
solc-select install all
solc-select use 0.7.3
solc-select use 0.7.3 --always-install
- name: Test with pytest
run: |
pytest tests/test_detectors.py

@ -0,0 +1,87 @@
---
name: CI
defaults:
run:
shell: bash
on:
workflow_dispatch:
pull_request:
paths:
- 'slither/tools/doctor/**'
- '.github/workflows/doctor.yml'
jobs:
slither-doctor:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest", "windows-2022"]
python: ["3.8", "3.9", "3.10", "3.11"]
exclude:
# strange failure
- os: windows-2022
python: 3.8
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
- name: Try system-wide Slither
run: |
echo "::group::Install slither"
pip3 install .
echo "::endgroup::"
# escape cwd so python doesn't pick up local module
cd /
echo "::group::Via module"
python3 -m slither.tools.doctor .
echo "::endgroup::"
echo "::group::Via binary"
slither-doctor .
echo "::endgroup::"
- name: Try user Slither
run: |
echo "::group::Install slither"
pip3 install --user .
echo "::endgroup::"
# escape cwd so python doesn't pick up local module
cd /
echo "::group::Via module"
python3 -m slither.tools.doctor .
echo "::endgroup::"
echo "::group::Via binary"
slither-doctor .
echo "::endgroup::"
- name: Try venv Slither
run: |
echo "::group::Install slither"
python3 -m venv venv
source venv/bin/activate || source venv/Scripts/activate
hash -r
pip3 install .
echo "::endgroup::"
# escape cwd so python doesn't pick up local module
cd /
echo "::group::Via module"
python3 -m slither.tools.doctor .
echo "::endgroup::"
echo "::group::Via binary"
slither-doctor .
echo "::endgroup::"

@ -35,8 +35,7 @@ jobs:
run: |
pip install ".[dev]"
solc-select install all
solc-select use 0.8.0
solc-select use 0.8.0 --always-install
cd tests/test_node_modules/
npm install hardhat

@ -26,7 +26,7 @@ jobs:
python -m venv /tmp/pip-audit-env
source /tmp/pip-audit-env/bin/activate
python -m pip install --upgrade pip
python -m pip install --upgrade pip setuptools wheel
python -m pip install .
- name: Run pip-audit

@ -0,0 +1,6 @@
* @montyly @0xalpharush @smonicas
/slither/tools/read_storage/ @0xalpharush
/slither/tools/doctor/ @elopez
/slither/slithir/ @montyly
/slither/analyses/ @montyly
/.github/workflows/ @elopez

@ -64,7 +64,10 @@ For each new detector, at least one regression tests must be present.
- If updating an existing detector, identify the respective json artifacts and then delete them, or run `python ./tests/test_detectors.py --overwrite` instead.
- Run `pytest ./tests/test_detectors.py` and check that everything worked.
To see the tests coverage, run `pytest tests/test_detectors.py --cov=slither/detectors --cov-branch --cov-report html`
To see the tests coverage, run `pytest tests/test_detectors.py --cov=slither/detectors --cov-branch --cov-report html`.
To run tests for a specific detector, run `pytest tests/test_detectors.py -k ReentrancyReadBeforeWritten` (the detector's class name is the argument).
To run tests for a specific version, run `pytest tests/test_detectors.py -k 0.7.6`.
The IDs of tests can be inspected using `pytest tests/test_detectors.py --collect-only`.
### Parser tests
- Create a test in `tests/ast-parsing`
@ -73,6 +76,10 @@ To see the tests coverage, run `pytest tests/test_detectors.py --cov=slither/d
- Run `pytest ./tests/test_ast_parsing.py` and check that everything worked.
To see the tests coverage, run `pytest tests/test_ast_parsing.py --cov=slither/solc_parsing --cov-branch --cov-report html`
To run tests for a specific test case, run `pytest tests/test_ast_parsing.py -k user_defined_value_type` (the filename is the argument).
To run tests for a specific version, run `pytest tests/test_ast_parsing.py -k 0.8.12`.
To run tests for a specific compiler json format, run `pytest tests/test_ast_parsing.py -k legacy` (can be legacy or compact).
The IDs of tests can be inspected using ``pytest tests/test_ast_parsing.py --collect-only`.
### Synchronization with crytic-compile
By default, `slither` follows either the latest version of crytic-compile in pip, or `crytic-compile@master` (look for dependencies in [`setup.py`](./setup.py). If crytic-compile development comes with breaking changes, the process to update `slither` is:

@ -2,6 +2,7 @@
FROM ubuntu:jammy AS python-wheels
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
gcc \
git \
python3-dev \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
@ -9,7 +10,7 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-ins
COPY . /slither
RUN cd /slither && \
echo pip3 install --no-cache-dir --upgrade pip && \
pip3 install --no-cache-dir --upgrade pip && \
pip3 wheel -w /wheels . solc-select pip setuptools wheel
@ -44,7 +45,7 @@ ENV PATH="/home/slither/.local/bin:${PATH}"
# no-index ensures we install the freshly-built wheels
RUN --mount=type=bind,target=/mnt,source=/wheels,from=python-wheels \
pip3 install --user --no-cache-dir --upgrade --no-index --find-links /mnt pip slither-analyzer solc-select
pip3 install --user --no-cache-dir --upgrade --no-index --find-links /mnt --no-deps /mnt/*.whl
RUN solc-select install 0.4.25 && solc-select use 0.4.25

@ -5,15 +5,24 @@
mkdir etherscan
cd etherscan || exit 255
if ! slither 0x7F37f78cBD74481E593F9C737776F7113d76B315 --etherscan-apikey "$GITHUB_ETHERSCAN"; then
echo "Etherscan test failed"
echo "::group::Etherscan mainnet"
if ! slither 0x7F37f78cBD74481E593F9C737776F7113d76B315 --etherscan-apikey "$GITHUB_ETHERSCAN" --no-fail-pedantic; then
echo "Etherscan mainnet test failed"
exit 1
fi
echo "::endgroup::"
if ! slither rinkeby:0xFe05820C5A92D9bc906D4A46F662dbeba794d3b7 --etherscan-apikey "$GITHUB_ETHERSCAN"; then
echo "Etherscan test failed"
# Perform a small sleep when API key is not available (e.g. on PR CI from external contributor)
if [ "$GITHUB_ETHERSCAN" = "" ]; then
sleep $(( ( RANDOM % 5 ) + 1 ))s
fi
echo "::group::Etherscan rinkeby"
if ! slither rinkeby:0xFe05820C5A92D9bc906D4A46F662dbeba794d3b7 --etherscan-apikey "$GITHUB_ETHERSCAN" --no-fail-pedantic; then
echo "Etherscan rinkeby test failed"
exit 1
fi
echo "::endgroup::"
exit 0

@ -12,10 +12,11 @@ setup(
packages=find_packages(),
python_requires=">=3.8",
install_requires=[
"packaging",
"prettytable>=0.7.2",
"pycryptodome>=3.4.6",
"crytic-compile>=0.2.4",
# "crytic-compile@git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile",
# "crytic-compile>=0.2.4",
"crytic-compile@git+https://github.com/crytic/crytic-compile.git@dev#egg=crytic-compile",
],
extras_require={
"dev": [
@ -46,6 +47,7 @@ setup(
"slither-mutate = slither.tools.mutator.__main__:main",
"slither-read-storage = slither.tools.read_storage.__main__:main",
"slither-doctor = slither.tools.doctor.__main__:main",
"slither-documentation = slither.tools.documentation.__main__:main",
]
},
)

@ -24,6 +24,7 @@ from slither.detectors.abstract_detector import AbstractDetector, DetectorClassi
from slither.printers import all_printers
from slither.printers.abstract_printer import AbstractPrinter
from slither.slither import Slither
from slither.utils import codex
from slither.utils.output import output_to_json, output_to_zip, output_to_sarif, ZIP_TYPES_ACCEPTED
from slither.utils.output_capture import StandardOutputCapture
from slither.utils.colors import red, set_colorization_enabled
@ -314,7 +315,6 @@ def parse_args(
"Checklist (consider using https://github.com/crytic/slither-action)"
)
group_misc = parser.add_argument_group("Additional options")
group_codex = parser.add_argument_group("Codex (https://beta.openai.com/docs/guides/code)")
group_detector.add_argument(
"--detect",
@ -555,47 +555,14 @@ def parse_args(
default=False,
)
group_codex.add_argument(
"--codex",
help="Enable codex (require an OpenAI API Key)",
action="store_true",
default=defaults_flag_in_config["codex"],
)
group_codex.add_argument(
"--codex-log",
help="Log codex queries (in crytic_export/codex/)",
group_misc.add_argument(
"--no-fail",
help="Do not fail in case of parsing (echidna mode only)",
action="store_true",
default=False,
)
group_codex.add_argument(
"--codex-contracts",
help="Comma separated list of contracts to submit to OpenAI Codex",
action="store",
default=defaults_flag_in_config["codex_contracts"],
default=defaults_flag_in_config["no_fail"],
)
group_codex.add_argument(
"--codex-model",
help="Name of the Codex model to use (affects pricing). Defaults to 'text-davinci-003'",
action="store",
default=defaults_flag_in_config["codex_model"],
)
group_codex.add_argument(
"--codex-temperature",
help="Temperature to use with Codex. Lower number indicates a more precise answer while higher numbers return more creative answers. Defaults to 0",
action="store",
default=defaults_flag_in_config["codex_temperature"],
)
group_codex.add_argument(
"--codex-max-tokens",
help="Maximum amount of tokens to use on the response. This number plus the size of the prompt can be no larger than the limit (4097 for text-davinci-003)",
action="store",
default=defaults_flag_in_config["codex_max_tokens"],
)
codex.init_parser(parser)
# debugger command
parser.add_argument("--debug", help=argparse.SUPPRESS, action="store_true", default=False)

@ -5,11 +5,11 @@ if TYPE_CHECKING:
class ChildFunction:
def __init__(self):
def __init__(self) -> None:
super().__init__()
self._function = None
def set_function(self, function: "Function"):
def set_function(self, function: "Function") -> None:
self._function = function
@property

@ -16,6 +16,7 @@ from slither.core.declarations import (
from slither.core.declarations.custom_error import CustomError
from slither.core.declarations.enum_top_level import EnumTopLevel
from slither.core.declarations.function_top_level import FunctionTopLevel
from slither.core.declarations.using_for_top_level import UsingForTopLevel
from slither.core.declarations.structure_top_level import StructureTopLevel
from slither.core.scope.scope import FileScope
from slither.core.variables.state_variable import StateVariable
@ -41,6 +42,7 @@ class SlitherCompilationUnit(Context):
self._enums_top_level: List[EnumTopLevel] = []
self._variables_top_level: List[TopLevelVariable] = []
self._functions_top_level: List[FunctionTopLevel] = []
self._using_for_top_level: List[UsingForTopLevel] = []
self._pragma_directives: List[Pragma] = []
self._import_directives: List[Import] = []
self._custom_errors: List[CustomError] = []
@ -205,6 +207,10 @@ class SlitherCompilationUnit(Context):
def functions_top_level(self) -> List[FunctionTopLevel]:
return self._functions_top_level
@property
def using_for_top_level(self) -> List[UsingForTopLevel]:
return self._using_for_top_level
@property
def custom_errors(self) -> List[CustomError]:
return self._custom_errors

@ -12,5 +12,8 @@ from .solidity_variables import (
)
from .structure import Structure
from .enum_contract import EnumContract
from .enum_top_level import EnumTopLevel
from .structure_contract import StructureContract
from .structure_top_level import StructureTopLevel
from .function_contract import FunctionContract
from .function_top_level import FunctionTopLevel

@ -70,6 +70,8 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
self._enums: Dict[str, "EnumContract"] = {}
self._structures: Dict[str, "StructureContract"] = {}
self._events: Dict[str, "Event"] = {}
# map accessible variable from name -> variable
# do not contain private variables inherited from contract
self._variables: Dict[str, "StateVariable"] = {}
self._variables_ordered: List["StateVariable"] = []
self._modifiers: Dict[str, "Modifier"] = {}
@ -79,6 +81,7 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
# The only str is "*"
self._using_for: Dict[Union[str, Type], List[Type]] = {}
self._using_for_complete: Dict[Union[str, Type], List[Type]] = None
self._kind: Optional[str] = None
self._is_interface: bool = False
self._is_library: bool = False
@ -266,6 +269,27 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
def using_for(self) -> Dict[Union[str, Type], List[Type]]:
return self._using_for
@property
def using_for_complete(self) -> Dict[Union[str, Type], List[Type]]:
"""
Dict[Union[str, Type], List[Type]]: Dict of merged local using for directive with top level directive
"""
def _merge_using_for(uf1, uf2):
result = {**uf1, **uf2}
for key, value in result.items():
if key in uf1 and key in uf2:
result[key] = value + uf1[key]
return result
if self._using_for_complete is None:
result = self.using_for
top_level_using_for = self.file_scope.using_for_directives
for uftl in top_level_using_for:
result = _merge_using_for(result, uftl.using_for)
self._using_for_complete = result
return self._using_for_complete
# endregion
###################################################################################
###################################################################################
@ -308,7 +332,9 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
@property
def variables(self) -> List["StateVariable"]:
"""
list(StateVariable): List of the state variables. Alias to self.state_variables
Returns all the accessible variables (do not include private variable from inherited contract)
list(StateVariable): List of the state variables. Alias to self.state_variables.
"""
return list(self.state_variables)
@ -319,6 +345,9 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
@property
def state_variables(self) -> List["StateVariable"]:
"""
Returns all the accessible variables (do not include private variable from inherited contract).
Use state_variables_ordered for all the variables following the storage order
list(StateVariable): List of the state variables.
"""
return list(self._variables.values())

@ -220,6 +220,9 @@ class Function(SourceMapping, metaclass=ABCMeta): # pylint: disable=too-many-pu
self._id: Optional[str] = None
# To be improved with a parsing of the documentation
self.has_documentation: bool = False
###################################################################################
###################################################################################
# region General properties

@ -0,0 +1,18 @@
from typing import TYPE_CHECKING, List, Dict, Union
from slither.core.solidity_types.type import Type
from slither.core.declarations.top_level import TopLevel
if TYPE_CHECKING:
from slither.core.scope.scope import FileScope
class UsingForTopLevel(TopLevel):
def __init__(self, scope: "FileScope"):
super().__init__()
self._using_for: Dict[Union[str, Type], List[Type]] = {}
self.file_scope: "FileScope" = scope
@property
def using_for(self) -> Dict[Type, List[Type]]:
return self._using_for

@ -7,7 +7,7 @@ if TYPE_CHECKING:
class Identifier(ExpressionTyped):
def __init__(self, value):
def __init__(self, value) -> None:
super().__init__()
self._value: "Variable" = value
@ -15,5 +15,5 @@ class Identifier(ExpressionTyped):
def value(self) -> "Variable":
return self._value
def __str__(self):
def __str__(self) -> str:
return str(self._value)

@ -10,16 +10,25 @@ if TYPE_CHECKING:
class Literal(Expression):
def __init__(self, value, custom_type, subdenomination=None):
def __init__(
self, value: Union[int, str], custom_type: "Type", subdenomination: Optional[str] = None
):
super().__init__()
self._value: Union[int, str] = value
self._value = value
self._type = custom_type
self._subdenomination: Optional[str] = subdenomination
self._subdenomination = subdenomination
@property
def value(self) -> Union[int, str]:
return self._value
@property
def converted_value(self) -> Union[int, str]:
"""Return the value of the literal, accounting for subdenomination e.g. ether"""
if self.subdenomination:
return convert_subdenomination(self._value, self.subdenomination)
return self._value
@property
def type(self) -> "Type":
return self._type
@ -28,9 +37,9 @@ class Literal(Expression):
def subdenomination(self) -> Optional[str]:
return self._subdenomination
def __str__(self):
def __str__(self) -> str:
if self.subdenomination:
return str(convert_subdenomination(self._value, self.subdenomination))
return str(self.converted_value)
if self.type in Int + Uint + Fixed + Ufixed + ["address"]:
return str(convert_string_to_int(self._value))
@ -38,7 +47,7 @@ class Literal(Expression):
# be sure to handle any character
return str(self._value)
def __eq__(self, other):
def __eq__(self, other) -> bool:
if not isinstance(other, Literal):
return False
return (self.value, self.subdenomination) == (other.value, other.subdenomination)

@ -1,10 +1,14 @@
from typing import List, Any, Dict, Optional, Union, Set
from typing import List, Any, Dict, Optional, Union, Set, TypeVar, Callable
from crytic_compile import CompilationUnit
from crytic_compile.source_unit import SourceUnit
from crytic_compile.utils.naming import Filename
from slither.core.declarations import Contract, Import, Pragma
from slither.core.declarations.custom_error_top_level import CustomErrorTopLevel
from slither.core.declarations.enum_top_level import EnumTopLevel
from slither.core.declarations.function_top_level import FunctionTopLevel
from slither.core.declarations.using_for_top_level import UsingForTopLevel
from slither.core.declarations.structure_top_level import StructureTopLevel
from slither.core.solidity_types import TypeAlias
from slither.core.variables.top_level_variable import TopLevelVariable
@ -35,6 +39,7 @@ class FileScope:
# Because we parse the function signature later on
# So we simplify the logic and have the scope fields all populated
self.functions: Set[FunctionTopLevel] = set()
self.using_for_directives: Set[UsingForTopLevel] = set()
self.imports: Set[Import] = set()
self.pragmas: Set[Pragma] = set()
self.structures: Dict[str, StructureTopLevel] = {}
@ -72,6 +77,9 @@ class FileScope:
if not new_scope.functions.issubset(self.functions):
self.functions |= new_scope.functions
learn_something = True
if not new_scope.using_for_directives.issubset(self.using_for_directives):
self.using_for_directives |= new_scope.using_for_directives
learn_something = True
if not new_scope.imports.issubset(self.imports):
self.imports |= new_scope.imports
learn_something = True
@ -98,6 +106,117 @@ class FileScope:
return self.contracts.get(name.name, None)
return self.contracts.get(name, None)
AbstractReturnType = TypeVar("AbstractReturnType")
def _generic_source_unit_getter(
self,
crytic_compile_compilation_unit: CompilationUnit,
name: str,
getter: Callable[[SourceUnit], Dict[str, AbstractReturnType]],
) -> Optional[AbstractReturnType]:
assert self.filename in crytic_compile_compilation_unit.source_units
source_unit = crytic_compile_compilation_unit.source_unit(self.filename)
if name in getter(source_unit):
return getter(source_unit)[name]
for scope in self.accessible_scopes:
source_unit = crytic_compile_compilation_unit.source_unit(scope.filename)
if name in getter(source_unit):
return getter(source_unit)[name]
return None
def bytecode_init(
self, crytic_compile_compilation_unit: CompilationUnit, contract_name: str
) -> Optional[str]:
"""
Return the init bytecode
Args:
crytic_compile_compilation_unit:
contract_name:
Returns:
"""
getter: Callable[[SourceUnit], Dict[str, str]] = lambda x: x.bytecodes_init
return self._generic_source_unit_getter(
crytic_compile_compilation_unit, contract_name, getter
)
def bytecode_runtime(
self, crytic_compile_compilation_unit: CompilationUnit, contract_name: str
) -> Optional[str]:
"""
Return the runtime bytecode
Args:
crytic_compile_compilation_unit:
contract_name:
Returns:
"""
getter: Callable[[SourceUnit], Dict[str, str]] = lambda x: x.bytecodes_runtime
return self._generic_source_unit_getter(
crytic_compile_compilation_unit, contract_name, getter
)
def srcmap_init(
self, crytic_compile_compilation_unit: CompilationUnit, contract_name: str
) -> Optional[List[str]]:
"""
Return the init scrmap
Args:
crytic_compile_compilation_unit:
contract_name:
Returns:
"""
getter: Callable[[SourceUnit], Dict[str, List[str]]] = lambda x: x.srcmaps_init
return self._generic_source_unit_getter(
crytic_compile_compilation_unit, contract_name, getter
)
def srcmap_runtime(
self, crytic_compile_compilation_unit: CompilationUnit, contract_name: str
) -> Optional[List[str]]:
"""
Return the runtime srcmap
Args:
crytic_compile_compilation_unit:
contract_name:
Returns:
"""
getter: Callable[[SourceUnit], Dict[str, List[str]]] = lambda x: x.srcmaps_runtime
return self._generic_source_unit_getter(
crytic_compile_compilation_unit, contract_name, getter
)
def abi(self, crytic_compile_compilation_unit: CompilationUnit, contract_name: str) -> Any:
"""
Return the abi
Args:
crytic_compile_compilation_unit:
contract_name:
Returns:
"""
getter: Callable[[SourceUnit], Dict[str, List[str]]] = lambda x: x.abis
return self._generic_source_unit_getter(
crytic_compile_compilation_unit, contract_name, getter
)
# region Built in definitions
###################################################################################
###################################################################################

@ -92,6 +92,10 @@ class SlitherCore(Context):
# But we allow to alter this (ex: file.sol:1) for vscode integration
self.line_prefix: str = "#"
# Use by the echidna printer
# If true, partial analysis is allowed
self.no_fail = False
@property
def compilation_units(self) -> List[SlitherCompilationUnit]:
return list(self._compilation_units)

@ -11,11 +11,11 @@ from slither.core.declarations.structure import Structure
class LocalVariable(ChildFunction, Variable):
def __init__(self):
def __init__(self) -> None:
super().__init__()
self._location: Optional[str] = None
def set_location(self, loc: str):
def set_location(self, loc: str) -> None:
self._location = loc
@property

@ -19,7 +19,8 @@ from .reentrancy.reentrancy_eth import ReentrancyEth
from .reentrancy.reentrancy_no_gas import ReentrancyNoGas
from .reentrancy.reentrancy_events import ReentrancyEvent
from .variables.unused_state_variables import UnusedStateVars
from .variables.possible_const_state_variables import ConstCandidateStateVars
from .variables.could_be_constant import CouldBeConstant
from .variables.could_be_immutable import CouldBeImmutable
from .statements.tx_origin import TxOrigin
from .statements.assembly import Assembly
from .operations.low_level_calls import LowLevelCalls

@ -32,7 +32,7 @@ class ConstantPragma(AbstractDetector):
info = ["Different versions of Solidity are used:\n"]
info += [f"\t- Version used: {[str(v) for v in versions]}\n"]
for p in pragma:
for p in sorted(pragma, key=lambda x: x.version):
info += ["\t- ", p, "\n"]
res = self.generate_result(info)

@ -0,0 +1,45 @@
from typing import List, Dict
from slither.utils.output import Output
from slither.core.compilation_unit import SlitherCompilationUnit
from slither.formatters.variables.unchanged_state_variables import custom_format
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from .unchanged_state_variables import UnchangedStateVariables
class CouldBeConstant(AbstractDetector):
"""
State variables that could be declared as constant.
Not all types for constants are implemented in Solidity as of 0.4.25.
The only supported types are value types and strings (ElementaryType).
Reference: https://solidity.readthedocs.io/en/latest/contracts.html#constant-state-variables
"""
ARGUMENT = "constable-states"
HELP = "State variables that could be declared constant"
IMPACT = DetectorClassification.OPTIMIZATION
CONFIDENCE = DetectorClassification.HIGH
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#state-variables-that-could-be-declared-constant"
WIKI_TITLE = "State variables that could be declared constant"
WIKI_DESCRIPTION = "State variables that are not updated following deployment should be declared constant to save gas."
WIKI_RECOMMENDATION = "Add the `constant` attribute to state variables that never change."
def _detect(self) -> List[Output]:
"""Detect state variables that could be constant"""
results = {}
unchanged_state_variables = UnchangedStateVariables(self.compilation_unit)
unchanged_state_variables.detect()
for variable in unchanged_state_variables.constant_candidates:
results[variable.canonical_name] = self.generate_result(
[variable, " should be constant \n"]
)
# Order by canonical name for deterministic results
return [results[k] for k in sorted(results)]
@staticmethod
def _format(compilation_unit: SlitherCompilationUnit, result: Dict) -> None:
custom_format(compilation_unit, result, "constant")

@ -0,0 +1,44 @@
from typing import List, Dict
from slither.utils.output import Output
from slither.core.compilation_unit import SlitherCompilationUnit
from slither.formatters.variables.unchanged_state_variables import custom_format
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from .unchanged_state_variables import UnchangedStateVariables
class CouldBeImmutable(AbstractDetector):
"""
State variables that could be declared immutable.
# Immutable attribute available in Solidity 0.6.5 and above
# https://blog.soliditylang.org/2020/04/06/solidity-0.6.5-release-announcement/
"""
# VULNERABLE_SOLC_VERSIONS =
ARGUMENT = "immutable-states"
HELP = "State variables that could be declared immutable"
IMPACT = DetectorClassification.OPTIMIZATION
CONFIDENCE = DetectorClassification.HIGH
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#state-variables-that-could-be-declared-immutable"
WIKI_TITLE = "State variables that could be declared immutable"
WIKI_DESCRIPTION = "State variables that are not updated following deployment should be declared immutable to save gas."
WIKI_RECOMMENDATION = "Add the `immutable` attribute to state variables that never change or are set only in the constructor."
def _detect(self) -> List[Output]:
"""Detect state variables that could be immutable"""
results = {}
unchanged_state_variables = UnchangedStateVariables(self.compilation_unit)
unchanged_state_variables.detect()
for variable in unchanged_state_variables.immutable_candidates:
results[variable.canonical_name] = self.generate_result(
[variable, " should be immutable \n"]
)
# Order by canonical name for deterministic results
return [results[k] for k in sorted(results)]
@staticmethod
def _format(compilation_unit: SlitherCompilationUnit, result: Dict) -> None:
custom_format(compilation_unit, result, "immutable")

@ -1,125 +0,0 @@
"""
Module detecting state variables that could be declared as constant
"""
from typing import Set, List, Dict
from slither.core.compilation_unit import SlitherCompilationUnit
from slither.core.solidity_types.elementary_type import ElementaryType
from slither.core.solidity_types.user_defined_type import UserDefinedType
from slither.core.variables.variable import Variable
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.utils.output import Output
from slither.visitors.expression.export_values import ExportValues
from slither.core.declarations import Contract, Function
from slither.core.declarations.solidity_variables import SolidityFunction
from slither.core.variables.state_variable import StateVariable
from slither.formatters.variables.possible_const_state_variables import custom_format
def _is_valid_type(v: StateVariable) -> bool:
t = v.type
if isinstance(t, ElementaryType):
return True
if isinstance(t, UserDefinedType) and isinstance(t.type, Contract):
return True
return False
def _valid_candidate(v: StateVariable) -> bool:
return _is_valid_type(v) and not (v.is_constant or v.is_immutable)
def _is_constant_var(v: Variable) -> bool:
if isinstance(v, StateVariable):
return v.is_constant
return False
class ConstCandidateStateVars(AbstractDetector):
"""
State variables that could be declared as constant detector.
Not all types for constants are implemented in Solidity as of 0.4.25.
The only supported types are value types and strings (ElementaryType).
Reference: https://solidity.readthedocs.io/en/latest/contracts.html#constant-state-variables
"""
ARGUMENT = "constable-states"
HELP = "State variables that could be declared constant"
IMPACT = DetectorClassification.OPTIMIZATION
CONFIDENCE = DetectorClassification.HIGH
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#state-variables-that-could-be-declared-constant"
WIKI_TITLE = "State variables that could be declared constant"
WIKI_DESCRIPTION = "Constant state variables should be declared constant to save gas."
WIKI_RECOMMENDATION = "Add the `constant` attributes to state variables that never change."
# https://solidity.readthedocs.io/en/v0.5.2/contracts.html#constant-state-variables
valid_solidity_function = [
SolidityFunction("keccak256()"),
SolidityFunction("keccak256(bytes)"),
SolidityFunction("sha256()"),
SolidityFunction("sha256(bytes)"),
SolidityFunction("ripemd160()"),
SolidityFunction("ripemd160(bytes)"),
SolidityFunction("ecrecover(bytes32,uint8,bytes32,bytes32)"),
SolidityFunction("addmod(uint256,uint256,uint256)"),
SolidityFunction("mulmod(uint256,uint256,uint256)"),
]
def _constant_initial_expression(self, v: Variable) -> bool:
if not v.expression:
return True
export = ExportValues(v.expression)
values = export.result()
if not values:
return True
if all((val in self.valid_solidity_function or _is_constant_var(val) for val in values)):
return True
return False
def _detect(self) -> List[Output]:
"""Detect state variables that could be const"""
results = []
all_variables_l = [c.state_variables for c in self.compilation_unit.contracts]
all_variables: Set[StateVariable] = {
item for sublist in all_variables_l for item in sublist
}
all_non_constant_elementary_variables = {v for v in all_variables if _valid_candidate(v)}
all_functions_nested = [c.all_functions_called for c in self.compilation_unit.contracts]
all_functions = list(
{
item1
for sublist in all_functions_nested
for item1 in sublist
if isinstance(item1, Function)
}
)
all_variables_written = [
f.state_variables_written for f in all_functions if not f.is_constructor_variables
]
all_variables_written = {item for sublist in all_variables_written for item in sublist}
constable_variables: List[Variable] = [
v
for v in all_non_constant_elementary_variables
if (v not in all_variables_written) and self._constant_initial_expression(v)
]
# Order for deterministic results
constable_variables = sorted(constable_variables, key=lambda x: x.canonical_name)
# Create a result for each finding
for v in constable_variables:
info = [v, " should be constant\n"]
json = self.generate_result(info)
results.append(json)
return results
@staticmethod
def _format(compilation_unit: SlitherCompilationUnit, result: Dict) -> None:
custom_format(compilation_unit, result)

@ -0,0 +1,125 @@
"""
Module detecting state variables that could be declared as constant
"""
from typing import Set, List
from packaging import version
from slither.core.compilation_unit import SlitherCompilationUnit
from slither.core.solidity_types.elementary_type import ElementaryType
from slither.core.solidity_types.user_defined_type import UserDefinedType
from slither.core.variables.variable import Variable
from slither.visitors.expression.export_values import ExportValues
from slither.core.declarations import Contract, Function
from slither.core.declarations.solidity_variables import SolidityFunction
from slither.core.variables.state_variable import StateVariable
from slither.core.expressions import CallExpression, NewContract
def _is_valid_type(v: StateVariable) -> bool:
t = v.type
if isinstance(t, ElementaryType):
return True
if isinstance(t, UserDefinedType) and isinstance(t.type, Contract):
return True
return False
def _valid_candidate(v: StateVariable) -> bool:
return _is_valid_type(v) and not (v.is_constant or v.is_immutable)
def _is_constant_var(v: Variable) -> bool:
if isinstance(v, StateVariable):
return v.is_constant
return False
# https://solidity.readthedocs.io/en/v0.5.2/contracts.html#constant-state-variables
valid_solidity_function = [
SolidityFunction("keccak256()"),
SolidityFunction("keccak256(bytes)"),
SolidityFunction("sha256()"),
SolidityFunction("sha256(bytes)"),
SolidityFunction("ripemd160()"),
SolidityFunction("ripemd160(bytes)"),
SolidityFunction("ecrecover(bytes32,uint8,bytes32,bytes32)"),
SolidityFunction("addmod(uint256,uint256,uint256)"),
SolidityFunction("mulmod(uint256,uint256,uint256)"),
]
def _constant_initial_expression(v: Variable) -> bool:
if not v.expression:
return True
# B b = new B(); b cannot be constant, so filter out and recommend it be immutable
if isinstance(v.expression, CallExpression) and isinstance(v.expression.called, NewContract):
return False
export = ExportValues(v.expression)
values = export.result()
if not values:
return True
return all((val in valid_solidity_function or _is_constant_var(val) for val in values))
class UnchangedStateVariables:
"""
Find state variables that could be declared as constant or immutable (not written after deployment).
"""
def __init__(self, compilation_unit: SlitherCompilationUnit):
self.compilation_unit = compilation_unit
self._constant_candidates: List[StateVariable] = []
self._immutable_candidates: List[StateVariable] = []
@property
def immutable_candidates(self) -> List[StateVariable]:
"""Return the immutable candidates"""
return self._immutable_candidates
@property
def constant_candidates(self) -> List[StateVariable]:
"""Return the constant candidates"""
return self._constant_candidates
def detect(self):
"""Detect state variables that could be constant or immutable"""
for c in self.compilation_unit.contracts_derived:
variables = []
functions = []
variables.append(c.state_variables)
functions.append(c.all_functions_called)
valid_candidates: Set[StateVariable] = {
item for sublist in variables for item in sublist if _valid_candidate(item)
}
all_functions: List[Function] = list(
{item1 for sublist in functions for item1 in sublist if isinstance(item1, Function)}
)
variables_written = []
constructor_variables_written = []
variables_initialized = []
for f in all_functions:
if f.is_constructor_variables:
variables_initialized.extend(f.state_variables_written)
elif f.is_constructor:
constructor_variables_written.extend(f.state_variables_written)
else:
variables_written.extend(f.state_variables_written)
for v in valid_candidates:
if v not in variables_written:
if _constant_initial_expression(v) and v not in constructor_variables_written:
self.constant_candidates.append(v)
elif (
v in constructor_variables_written or v in variables_initialized
) and version.parse(self.compilation_unit.solc_version) >= version.parse(
"0.6.5"
):
self.immutable_candidates.append(v)

@ -5,7 +5,7 @@ from slither.formatters.exceptions import FormatError, FormatImpossible
from slither.formatters.utils.patches import create_patch
def custom_format(compilation_unit: SlitherCompilationUnit, result):
def custom_format(compilation_unit: SlitherCompilationUnit, result, attribute: str) -> None:
elements = result["elements"]
for element in elements:
@ -15,14 +15,14 @@ def custom_format(compilation_unit: SlitherCompilationUnit, result):
contract = scope.get_contract_from_name(contract_name)
var = contract.get_state_variable_from_name(element["name"])
if not var.expression:
raise FormatImpossible(f"{var.name} is uninitialized and cannot become constant.")
raise FormatImpossible(f"{var.name} is uninitialized and cannot become {attribute}.")
_patch(
compilation_unit,
result,
element["source_mapping"]["filename_absolute"],
element["name"],
"constant " + element["name"],
f"{attribute} " + element["name"],
element["source_mapping"]["start"],
element["source_mapping"]["start"] + element["source_mapping"]["length"],
)

@ -30,6 +30,7 @@ from slither.slithir.operations import (
)
from slither.slithir.operations.binary import Binary
from slither.slithir.variables import Constant
from slither.visitors.expression.constants_folding import ConstantFolding
def _get_name(f: Union[Function, Variable]) -> str:
@ -175,6 +176,11 @@ def _extract_constants_from_irs( # pylint: disable=too-many-branches,too-many-n
all_cst_used_in_binary[str(ir.type)].append(
ConstantValue(str(r.value), str(r.type))
)
if isinstance(ir.variable_left, Constant) and isinstance(ir.variable_right, Constant):
if ir.lvalue:
type_ = ir.lvalue.type
cst = ConstantFolding(ir.expression, type_).result()
all_cst_used.append(ConstantValue(str(cst.value), str(type_)))
if isinstance(ir, TypeConversion):
if isinstance(ir.variable, Constant):
all_cst_used.append(ConstantValue(str(ir.variable.value), str(ir.type)))
@ -323,6 +329,7 @@ def _call_a_parameter(slither: SlitherCore) -> Dict[str, List[Dict]]:
ret: Dict[str, List[Dict]] = defaultdict(list)
for contract in slither.contracts: # pylint: disable=too-many-nested-blocks
for function in contract.functions_entry_points:
try:
for ir in function.all_slithir_operations():
if isinstance(ir, HighLevelCall):
for idx, parameter in enumerate(function.parameters):
@ -344,6 +351,10 @@ def _call_a_parameter(slither: SlitherCore) -> Dict[str, List[Dict]]:
"signature": None,
}
)
except Exception as e:
if slither.no_fail:
continue
raise e
return ret

@ -21,14 +21,8 @@ def _extract_evm_info(slither):
CFG = load_evm_cfg_builder()
for contract in slither.contracts_derived:
contract_bytecode_runtime = (
contract.compilation_unit.crytic_compile_compilation_unit.bytecode_runtime(
contract.name
)
)
contract_srcmap_runtime = (
contract.compilation_unit.crytic_compile_compilation_unit.srcmap_runtime(contract.name)
)
contract_bytecode_runtime = contract.scope.bytecode_runtime(contract.name)
contract_srcmap_runtime = contract.scope.srcmap_runtime(contract.name)
cfg = CFG(contract_bytecode_runtime)
evm_info["cfg", contract.name] = cfg
evm_info["mapping", contract.name] = generate_source_to_evm_ins_mapping(
@ -38,12 +32,8 @@ def _extract_evm_info(slither):
contract.source_mapping.filename.absolute,
)
contract_bytecode_init = (
contract.compilation_unit.crytic_compile_compilation_unit.bytecode_init(contract.name)
)
contract_srcmap_init = (
contract.compilation_unit.crytic_compile_compilation_unit.srcmap_init(contract.name)
)
contract_bytecode_init = contract.scope.bytecode_init(contract.name)
contract_srcmap_init = contract.scope.srcmap_init(contract.name)
cfg_init = CFG(contract_bytecode_init)
evm_info["cfg_init", contract.name] = cfg_init

@ -91,6 +91,8 @@ class Slither(SlitherCore): # pylint: disable=too-many-instance-attributes
self.codex_max_tokens = kwargs.get("codex_max_tokens", 300)
self.codex_log = kwargs.get("codex_log", False)
self.no_fail = kwargs.get("no_fail", False)
self._parsers: List[SlitherCompilationUnitSolc] = []
try:
if isinstance(target, CryticCompile):
@ -128,41 +130,27 @@ class Slither(SlitherCore): # pylint: disable=too-many-instance-attributes
triage_mode = kwargs.get("triage_mode", False)
self._triage_mode = triage_mode
self._init_parsing_and_analyses(kwargs.get("skip_analyze", False))
def _init_parsing_and_analyses(self, skip_analyze: bool) -> None:
for parser in self._parsers:
try:
parser.parse_contracts()
except Exception as e:
if self.no_fail:
continue
raise e
# skip_analyze is only used for testing
if not kwargs.get("skip_analyze", False):
if not skip_analyze:
for parser in self._parsers:
try:
parser.analyze_contracts()
# def _init_from_raw_json(self, filename):
# if not os.path.isfile(filename):
# raise SlitherError(
# "{} does not exist (are you in the correct directory?)".format(filename)
# )
# assert filename.endswith("json")
# with open(filename, encoding="utf8") as astFile:
# stdout = astFile.read()
# if not stdout:
# to_log = f"Empty AST file: {filename}"
# raise SlitherError(to_log)
# contracts_json = stdout.split("\n=")
#
# self._parser = SlitherCompilationUnitSolc(filename, self)
#
# for c in contracts_json:
# self._parser.parse_top_level_from_json(c)
# def _init_from_list(self, contract):
# self._parser = SlitherCompilationUnitSolc("", self)
# for c in contract:
# if "absolutePath" in c:
# path = c["absolutePath"]
# else:
# path = c["attributes"]["absolutePath"]
# self._parser.parse_top_level_from_loaded_json(c, path)
except Exception as e:
if self.no_fail:
continue
raise e
@property
def detectors(self):

@ -15,6 +15,7 @@ from slither.core.declarations import (
)
from slither.core.declarations.custom_error import CustomError
from slither.core.declarations.function_contract import FunctionContract
from slither.core.declarations.function_top_level import FunctionTopLevel
from slither.core.declarations.solidity_import_placeholder import SolidityImportPlaceHolder
from slither.core.declarations.solidity_variables import SolidityCustomRevert
from slither.core.expressions import Identifier, Literal
@ -199,18 +200,22 @@ def _fits_under_byte(val: Union[int, str]) -> List[str]:
return [f"bytes{f}" for f in range(length, 33)] + ["bytes"]
def _find_function_from_parameter(ir: Call, candidates: List[Function]) -> Optional[Function]:
def _find_function_from_parameter(
arguments: List[Variable], candidates: List[Function], full_comparison: bool
) -> Optional[Function]:
"""
Look for a function in candidates that can be the target of the ir's call
Look for a function in candidates that can be the target based on the ir's call arguments
Try the implicit type conversion for uint/int/bytes. Constant values can be both uint/int
While variables stick to their base type, but can changed the size
While variables stick to their base type, but can changed the size.
If full_comparison is True it will do a comparison of all the arguments regardless if
the candidate remained is one.
:param ir:
:param arguments:
:param candidates:
:param full_comparison:
:return:
"""
arguments = ir.arguments
type_args: List[str]
for idx, arg in enumerate(arguments):
if isinstance(arg, (list,)):
@ -258,7 +263,7 @@ def _find_function_from_parameter(ir: Call, candidates: List[Function]) -> Optio
not_found = False
candidates_kept.append(candidate)
if len(candidates_kept) == 1:
if len(candidates_kept) == 1 and not full_comparison:
return candidates_kept[0]
candidates = candidates_kept
if len(candidates) == 1:
@ -374,7 +379,7 @@ def integrate_value_gas(result):
###################################################################################
def propagate_type_and_convert_call(result, node):
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
"""
@ -450,19 +455,25 @@ def propagate_type_and_convert_call(result, node):
return result
def _convert_type_contract(ir, compilation_unit: "SlitherCompilationUnit"):
def _convert_type_contract(ir: Member) -> Assignment:
assert isinstance(ir.variable_left.type, TypeInformation)
contract = ir.variable_left.type.type
scope = ir.node.file_scope
if ir.variable_right == "creationCode":
bytecode = compilation_unit.crytic_compile_compilation_unit.bytecode_init(contract.name)
bytecode = scope.bytecode_init(
ir.node.compilation_unit.crytic_compile_compilation_unit, contract.name
)
assignment = Assignment(ir.lvalue, Constant(str(bytecode)), ElementaryType("bytes"))
assignment.set_expression(ir.expression)
assignment.set_node(ir.node)
assignment.lvalue.set_type(ElementaryType("bytes"))
return assignment
if ir.variable_right == "runtimeCode":
bytecode = compilation_unit.crytic_compile_compilation_unit.bytecode_runtime(contract.name)
bytecode = scope.bytecode_runtime(
ir.node.compilation_unit.crytic_compile_compilation_unit, contract.name
)
assignment = Assignment(ir.lvalue, Constant(str(bytecode)), ElementaryType("bytes"))
assignment.set_expression(ir.expression)
assignment.set_node(ir.node)
@ -497,7 +508,9 @@ def propagate_types(ir, node: "Node"): # pylint: disable=too-many-locals
# propagate the type
node_function = node.function
using_for = (
node_function.contract.using_for if isinstance(node_function, FunctionContract) else {}
node_function.contract.using_for_complete
if isinstance(node_function, FunctionContract)
else {}
)
if isinstance(ir, OperationWithLValue):
# Force assignment in case of missing previous correct type
@ -524,9 +537,9 @@ def propagate_types(ir, node: "Node"): # pylint: disable=too-many-locals
if can_be_solidity_func(ir):
return convert_to_solidity_func(ir)
# convert library
# convert library or top level function
if t in using_for or "*" in using_for:
new_ir = convert_to_library(ir, node, using_for)
new_ir = convert_to_library_or_top_level(ir, node, using_for)
if new_ir:
return new_ir
@ -534,8 +547,8 @@ def propagate_types(ir, node: "Node"): # pylint: disable=too-many-locals
# UserdefinedType
t_type = t.type
if isinstance(t_type, Contract):
contract = node.file_scope.get_contract_from_name(t_type.name)
return convert_type_of_high_and_internal_level_call(ir, contract)
# the target contract of the IR is the t_type (the destination of the call)
return convert_type_of_high_and_internal_level_call(ir, t_type)
# Convert HighLevelCall to LowLevelCall
if (isinstance(t, ElementaryType) and t.name == "address") or (
@ -544,6 +557,7 @@ def propagate_types(ir, node: "Node"): # pylint: disable=too-many-locals
# Cannot be a top level function with this.
assert isinstance(node_function, FunctionContract)
if ir.destination.name == "this":
# the target contract is the contract itself
return convert_type_of_high_and_internal_level_call(
ir, node_function.contract
)
@ -578,6 +592,7 @@ def propagate_types(ir, node: "Node"): # pylint: disable=too-many-locals
function_contract = (
func.contract if isinstance(func, FunctionContract) else None
)
# the target contract might be None if its a top level function
convert_type_of_high_and_internal_level_call(ir, function_contract)
return_type = ir.function.return_type
if return_type:
@ -651,7 +666,24 @@ def propagate_types(ir, node: "Node"): # pylint: disable=too-many-locals
if ir.variable_right == "selector" and isinstance(ir.variable_left, (CustomError)):
assignment = Assignment(
ir.lvalue,
Constant(str(get_function_id(ir.variable_left.solidity_signature))),
Constant(
str(get_function_id(ir.variable_left.solidity_signature)),
ElementaryType("bytes4"),
),
ElementaryType("bytes4"),
)
assignment.set_expression(ir.expression)
assignment.set_node(ir.node)
assignment.lvalue.set_type(ElementaryType("bytes4"))
return assignment
if isinstance(ir.variable_right, (CustomError)):
assignment = Assignment(
ir.lvalue,
Constant(
str(get_function_id(ir.variable_left.solidity_signature)),
ElementaryType("bytes4"),
),
ElementaryType("bytes4"),
)
assignment.set_expression(ir.expression)
@ -673,7 +705,7 @@ def propagate_types(ir, node: "Node"): # pylint: disable=too-many-locals
if isinstance(ir.variable_left, TemporaryVariable) and isinstance(
ir.variable_left.type, TypeInformation
):
return _convert_type_contract(ir, node.function.compilation_unit)
return _convert_type_contract(ir)
left = ir.variable_left
t = None
ir_func = ir.function
@ -723,7 +755,7 @@ def propagate_types(ir, node: "Node"): # pylint: disable=too-many-locals
if f:
ir.lvalue.set_type(f)
else:
# Allow propgation for variable access through contract's nale
# Allow propgation for variable access through contract's name
# like Base_contract.my_variable
v = next(
(
@ -875,7 +907,9 @@ def extract_tmp_call(ins: TmpCall, contract: Optional[Contract]): # pylint: dis
# }
node_func = ins.node.function
using_for = (
node_func.contract.using_for if isinstance(node_func, FunctionContract) else {}
node_func.contract.using_for_complete
if isinstance(node_func, FunctionContract)
else {}
)
targeted_libraries = (
@ -888,6 +922,10 @@ def extract_tmp_call(ins: TmpCall, contract: Optional[Contract]): # pylint: dis
lib_contract_type.type, Contract
):
continue
if isinstance(lib_contract_type, FunctionContract):
# Using for with list of functions, this is the function called
candidates.append(lib_contract_type)
else:
lib_contract = lib_contract_type.type
for lib_func in lib_contract.functions:
if lib_func.name == ins.ori.variable_right:
@ -1320,8 +1358,31 @@ def convert_to_pop(ir, node):
return ret
def look_for_library(contract, ir, using_for, t):
def look_for_library_or_top_level(contract, ir, using_for, t):
for destination in using_for[t]:
if isinstance(destination, FunctionTopLevel) and destination.name == ir.function_name:
arguments = [ir.destination] + ir.arguments
if (
len(destination.parameters) == len(arguments)
and _find_function_from_parameter(arguments, [destination], True) is not None
):
internalcall = InternalCall(destination, ir.nbr_arguments, ir.lvalue, ir.type_call)
internalcall.set_expression(ir.expression)
internalcall.set_node(ir.node)
internalcall.arguments = [ir.destination] + ir.arguments
return_type = internalcall.function.return_type
if return_type:
if len(return_type) == 1:
internalcall.lvalue.set_type(return_type[0])
elif len(return_type) > 1:
internalcall.lvalue.set_type(return_type)
else:
internalcall.lvalue = None
return internalcall
if isinstance(destination, FunctionContract) and destination.contract.is_library:
lib_contract = destination.contract
else:
lib_contract = contract.file_scope.get_contract_from_name(str(destination))
if lib_contract:
lib_call = LibraryCall(
@ -1342,19 +1403,19 @@ def look_for_library(contract, ir, using_for, t):
return None
def convert_to_library(ir, node, using_for):
def convert_to_library_or_top_level(ir, node, using_for):
# We use contract_declarer, because Solidity resolve the library
# before resolving the inheritance.
# Though we could use .contract as libraries cannot be shadowed
contract = node.function.contract_declarer
t = ir.destination.type
if t in using_for:
new_ir = look_for_library(contract, ir, using_for, t)
new_ir = look_for_library_or_top_level(contract, ir, using_for, t)
if new_ir:
return new_ir
if "*" in using_for:
new_ir = look_for_library(contract, ir, using_for, "*")
new_ir = look_for_library_or_top_level(contract, ir, using_for, "*")
if new_ir:
return new_ir
@ -1400,7 +1461,7 @@ def convert_type_library_call(ir: HighLevelCall, lib_contract: Contract):
# TODO: handle collision with multiple state variables/functions
func = lib_contract.get_state_variable_from_name(ir.function_name)
if func is None and candidates:
func = _find_function_from_parameter(ir, candidates)
func = _find_function_from_parameter(ir.arguments, candidates, False)
# In case of multiple binding to the same type
# TODO: this part might not be needed with _find_function_from_parameter
@ -1470,7 +1531,19 @@ def _convert_to_structure_to_list(return_type: Type) -> List[Type]:
return [return_type.type]
def convert_type_of_high_and_internal_level_call(ir: Operation, contract: Optional[Contract]):
def convert_type_of_high_and_internal_level_call(
ir: Operation, contract: Optional[Contract]
) -> Optional[Operation]:
"""
Convert the IR type based on heuristic
Args:
ir: target
contract: optional contract. This should be the target of the IR. It will be used to look up potential functions
Returns:
Potential new IR
"""
func = None
if isinstance(ir, InternalCall):
candidates: List[Function]
@ -1496,7 +1569,7 @@ def convert_type_of_high_and_internal_level_call(ir: Operation, contract: Option
if f.name == ir.function_name and len(f.parameters) == len(ir.arguments)
]
func = _find_function_from_parameter(ir, candidates)
func = _find_function_from_parameter(ir.arguments, candidates, False)
if not func:
assert contract
@ -1504,7 +1577,6 @@ def convert_type_of_high_and_internal_level_call(ir: Operation, contract: Option
else:
assert isinstance(ir, HighLevelCall)
assert contract
candidates = [
f
for f in contract.functions
@ -1512,14 +1584,13 @@ def convert_type_of_high_and_internal_level_call(ir: Operation, contract: Option
and not f.is_shadowed
and len(f.parameters) == len(ir.arguments)
]
if len(candidates) == 1:
func = candidates[0]
if func is None:
# TODO: handle collision with multiple state variables/functions
func = contract.get_state_variable_from_name(ir.function_name)
if func is None and candidates:
func = _find_function_from_parameter(ir, candidates)
func = _find_function_from_parameter(ir.arguments, candidates, False)
# lowlelvel lookup needs to be done at last step
if not func:
@ -1777,7 +1848,7 @@ def _find_source_mapping_references(irs: List[Operation]):
###################################################################################
def apply_ir_heuristics(irs, node):
def apply_ir_heuristics(irs: List[Operation], node: "Node"):
"""
Apply a set of heuristic to improve slithIR
"""

@ -6,7 +6,7 @@ from slither.core.declarations import Modifier, Event, EnumContract, StructureCo
from slither.core.declarations.contract import Contract
from slither.core.declarations.custom_error_contract import CustomErrorContract
from slither.core.declarations.function_contract import FunctionContract
from slither.core.solidity_types import ElementaryType, TypeAliasContract
from slither.core.solidity_types import ElementaryType, TypeAliasContract, Type
from slither.core.variables.state_variable import StateVariable
from slither.solc_parsing.declarations.caller_context import CallerContextExpression
from slither.solc_parsing.declarations.custom_error import CustomErrorSolc
@ -326,7 +326,13 @@ class ContractSolc(CallerContextExpression):
def parse_state_variables(self):
for father in self._contract.inheritance_reverse:
self._contract.variables_as_dict.update(father.variables_as_dict)
self._contract.variables_as_dict.update(
{
name: v
for name, v in father.variables_as_dict.items()
if v.visibility != "private"
}
)
self._contract.add_variables_ordered(
[
var
@ -391,11 +397,11 @@ class ContractSolc(CallerContextExpression):
###################################################################################
###################################################################################
def log_incorrect_parsing(self, error):
def log_incorrect_parsing(self, error: str) -> None:
if self._contract.compilation_unit.core.disallow_partial:
raise ParsingError(error)
LOGGER.error(error)
self._contract.is_incorrectly_parsed = True
self._contract.is_incorrectly_constructed = True
def analyze_content_modifiers(self):
try:
@ -577,21 +583,26 @@ class ContractSolc(CallerContextExpression):
except (VariableNotFound, KeyError) as e:
self.log_incorrect_parsing(f"Missing state variable {e}")
def analyze_using_for(self):
def analyze_using_for(self): # pylint: disable=too-many-branches
try:
for father in self._contract.inheritance:
self._contract.using_for.update(father.using_for)
if self.is_compact_ast:
for using_for in self._usingForNotParsed:
lib_name = parse_type(using_for["libraryName"], self)
if "typeName" in using_for and using_for["typeName"]:
type_name = parse_type(using_for["typeName"], self)
else:
type_name = "*"
if type_name not in self._contract.using_for:
self._contract.using_for[type_name] = []
self._contract.using_for[type_name].append(lib_name)
if "libraryName" in using_for:
self._contract.using_for[type_name].append(
parse_type(using_for["libraryName"], self)
)
else:
# We have a list of functions. A function can be topLevel or a library function
self._analyze_function_list(using_for["functionList"], type_name)
else:
for using_for in self._usingForNotParsed:
children = using_for[self.get_children()]
@ -609,6 +620,59 @@ class ContractSolc(CallerContextExpression):
except (VariableNotFound, KeyError) as e:
self.log_incorrect_parsing(f"Missing using for {e}")
def _analyze_function_list(self, function_list: List, type_name: Type):
for f in function_list:
full_name_split = f["function"]["name"].split(".")
if len(full_name_split) == 1:
# Top level function
function_name = full_name_split[0]
self._analyze_top_level_function(function_name, type_name)
elif len(full_name_split) == 2:
# It can be a top level function behind an aliased import
# or a library function
first_part = full_name_split[0]
function_name = full_name_split[1]
self._check_aliased_import(first_part, function_name, type_name)
else:
# MyImport.MyLib.a we don't care of the alias
library_name = full_name_split[1]
function_name = full_name_split[2]
self._analyze_library_function(library_name, function_name, type_name)
def _check_aliased_import(self, first_part: str, function_name: str, type_name: Type):
# We check if the first part appear as alias for an import
# if it is then function_name must be a top level function
# otherwise it's a library function
for i in self._contract.file_scope.imports:
if i.alias == first_part:
self._analyze_top_level_function(function_name, type_name)
return
self._analyze_library_function(first_part, function_name, type_name)
def _analyze_top_level_function(self, function_name: str, type_name: Type):
for tl_function in self.compilation_unit.functions_top_level:
if tl_function.name == function_name:
self._contract.using_for[type_name].append(tl_function)
def _analyze_library_function(
self, library_name: str, function_name: str, type_name: Type
) -> None:
# Get the library function
found = False
for c in self.compilation_unit.contracts:
if found:
break
if c.name == library_name:
for f in c.functions:
if f.name == function_name:
self._contract.using_for[type_name].append(f)
found = True
break
if not found:
self.log_incorrect_parsing(
f"Contract level using for: Library {library_name} - function {function_name} not found"
)
def analyze_enums(self):
try:
for father in self._contract.inheritance:

@ -91,6 +91,9 @@ class FunctionSolc(CallerContextExpression):
Union[LocalVariableSolc, LocalVariableInitFromTupleSolc]
] = []
if "documentation" in function_data:
function.has_documentation = True
@property
def underlying_function(self) -> Function:
return self._function
@ -308,7 +311,7 @@ class FunctionSolc(CallerContextExpression):
for node_parser in self._node_to_yulobject.values():
node_parser.analyze_expressions()
self._filter_ternary()
self._rewrite_ternary_as_if_else()
self._remove_alone_endif()
@ -340,7 +343,6 @@ class FunctionSolc(CallerContextExpression):
node,
[self._function.name, f"asm_{len(self._node_to_yulobject)}"],
scope,
parent_func=self._function,
)
self._node_to_yulobject[node] = yul_object
return yul_object
@ -1336,7 +1338,7 @@ class FunctionSolc(CallerContextExpression):
###################################################################################
###################################################################################
def _filter_ternary(self) -> bool:
def _rewrite_ternary_as_if_else(self) -> bool:
ternary_found = True
updated = False
while ternary_found:

@ -87,7 +87,7 @@ class ModifierSolc(FunctionSolc):
for node in self._node_to_nodesolc.values():
node.analyze_expressions(self)
self._filter_ternary()
self._rewrite_ternary_as_if_else()
self._remove_alone_endif()
# self._analyze_read_write()

@ -0,0 +1,167 @@
"""
Using For Top Level module
"""
import logging
from typing import TYPE_CHECKING, Dict, Union
from slither.core.compilation_unit import SlitherCompilationUnit
from slither.core.declarations import (
StructureTopLevel,
EnumTopLevel,
)
from slither.core.declarations.using_for_top_level import UsingForTopLevel
from slither.core.scope.scope import FileScope
from slither.core.solidity_types import TypeAliasTopLevel
from slither.core.solidity_types.user_defined_type import UserDefinedType
from slither.solc_parsing.declarations.caller_context import CallerContextExpression
from slither.solc_parsing.solidity_types.type_parsing import parse_type
if TYPE_CHECKING:
from slither.solc_parsing.slither_compilation_unit_solc import SlitherCompilationUnitSolc
LOGGER = logging.getLogger("UsingForTopLevelSolc")
class UsingForTopLevelSolc(CallerContextExpression): # pylint: disable=too-few-public-methods
"""
UsingFor class
"""
def __init__(
self,
uftl: UsingForTopLevel,
top_level_data: Dict,
slither_parser: "SlitherCompilationUnitSolc",
) -> None:
self._type_name = top_level_data["typeName"]
self._global = top_level_data["global"]
if "libraryName" in top_level_data:
self._library_name = top_level_data["libraryName"]
else:
self._functions = top_level_data["functionList"]
self._library_name = None
self._using_for = uftl
self._slither_parser = slither_parser
def analyze(self) -> None:
type_name = parse_type(self._type_name, self)
self._using_for.using_for[type_name] = []
if self._library_name is not None:
library_name = parse_type(self._library_name, self)
self._using_for.using_for[type_name].append(library_name)
self._propagate_global(type_name)
else:
for f in self._functions:
full_name_split = f["function"]["name"].split(".")
if len(full_name_split) == 1:
# Top level function
function_name: str = full_name_split[0]
self._analyze_top_level_function(function_name, type_name)
elif len(full_name_split) == 2:
# It can be a top level function behind an aliased import
# or a library function
first_part = full_name_split[0]
function_name = full_name_split[1]
self._check_aliased_import(first_part, function_name, type_name)
else:
# MyImport.MyLib.a we don't care of the alias
library_name_str = full_name_split[1]
function_name = full_name_split[2]
self._analyze_library_function(library_name_str, function_name, type_name)
def _check_aliased_import(
self,
first_part: str,
function_name: str,
type_name: Union[TypeAliasTopLevel, UserDefinedType],
):
# We check if the first part appear as alias for an import
# if it is then function_name must be a top level function
# otherwise it's a library function
for i in self._using_for.file_scope.imports:
if i.alias == first_part:
self._analyze_top_level_function(function_name, type_name)
return
self._analyze_library_function(first_part, function_name, type_name)
def _analyze_top_level_function(
self, function_name: str, type_name: Union[TypeAliasTopLevel, UserDefinedType]
) -> None:
for tl_function in self.compilation_unit.functions_top_level:
if tl_function.name == function_name:
self._using_for.using_for[type_name].append(tl_function)
self._propagate_global(type_name)
break
def _analyze_library_function(
self,
library_name: str,
function_name: str,
type_name: Union[TypeAliasTopLevel, UserDefinedType],
) -> None:
found = False
for c in self.compilation_unit.contracts:
if found:
break
if c.name == library_name:
for cf in c.functions:
if cf.name == function_name:
self._using_for.using_for[type_name].append(cf)
self._propagate_global(type_name)
found = True
break
if not found:
LOGGER.warning(
f"Top level using for: Library {library_name} - function {function_name} not found"
)
def _propagate_global(self, type_name: Union[TypeAliasTopLevel, UserDefinedType]) -> None:
if self._global:
for scope in self.compilation_unit.scopes.values():
if isinstance(type_name, TypeAliasTopLevel):
for alias in scope.user_defined_types.values():
if alias == type_name:
scope.using_for_directives.add(self._using_for)
elif isinstance(type_name, UserDefinedType):
self._propagate_global_UserDefinedType(scope, type_name)
else:
LOGGER.error(
f"Error when propagating global using for {type_name} {type(type_name)}"
)
def _propagate_global_UserDefinedType(self, scope: FileScope, type_name: UserDefinedType):
underlying = type_name.type
if isinstance(underlying, StructureTopLevel):
for struct in scope.structures.values():
if struct == underlying:
scope.using_for_directives.add(self._using_for)
elif isinstance(underlying, EnumTopLevel):
for enum in scope.enums.values():
if enum == underlying:
scope.using_for_directives.add(self._using_for)
else:
LOGGER.error(
f"Error when propagating global {underlying} {type(underlying)} not a StructTopLevel or EnumTopLevel"
)
@property
def is_compact_ast(self) -> bool:
return self._slither_parser.is_compact_ast
@property
def compilation_unit(self) -> SlitherCompilationUnit:
return self._slither_parser.compilation_unit
def get_key(self) -> str:
return self._slither_parser.get_key()
@property
def slither_parser(self) -> "SlitherCompilationUnitSolc":
return self._slither_parser
@property
def underlying_using_for(self) -> UsingForTopLevel:
return self._using_for

@ -14,6 +14,7 @@ from slither.core.declarations.function_top_level import FunctionTopLevel
from slither.core.declarations.import_directive import Import
from slither.core.declarations.pragma_directive import Pragma
from slither.core.declarations.structure_top_level import StructureTopLevel
from slither.core.declarations.using_for_top_level import UsingForTopLevel
from slither.core.scope.scope import FileScope
from slither.core.solidity_types import ElementaryType, TypeAliasTopLevel
from slither.core.variables.top_level_variable import TopLevelVariable
@ -22,8 +23,10 @@ from slither.solc_parsing.declarations.contract import ContractSolc
from slither.solc_parsing.declarations.custom_error import CustomErrorSolc
from slither.solc_parsing.declarations.function import FunctionSolc
from slither.solc_parsing.declarations.structure_top_level import StructureTopLevelSolc
from slither.solc_parsing.declarations.using_for_top_level import UsingForTopLevelSolc
from slither.solc_parsing.exceptions import VariableNotFound
from slither.solc_parsing.variables.top_level_variable import TopLevelVariableSolc
from slither.solc_parsing.declarations.caller_context import CallerContextExpression
logging.basicConfig()
logger = logging.getLogger("SlitherSolcParsing")
@ -45,19 +48,25 @@ def _handle_import_aliases(
"""
for symbol_alias in symbol_aliases:
if (
"foreign" in symbol_alias
and "name" in symbol_alias["foreign"]
and "local" in symbol_alias
):
if "foreign" in symbol_alias and "local" in symbol_alias:
if isinstance(symbol_alias["foreign"], dict) and "name" in symbol_alias["foreign"]:
original_name = symbol_alias["foreign"]["name"]
local_name = symbol_alias["local"]
import_directive.renaming[local_name] = original_name
# Assuming that two imports cannot collide in renaming
scope.renaming[local_name] = original_name
# This path should only be hit for the malformed AST of solc 0.5.12 where
# the foreign identifier cannot be found but is required to resolve the alias.
# see https://github.com/crytic/slither/issues/1319
elif symbol_alias["local"]:
raise SlitherException(
"Cannot resolve local alias for import directive due to malformed AST. Please upgrade to solc 0.6.0 or higher."
)
class SlitherCompilationUnitSolc:
class SlitherCompilationUnitSolc(CallerContextExpression):
# pylint: disable=no-self-use,too-many-instance-attributes
def __init__(self, compilation_unit: SlitherCompilationUnit):
super().__init__()
@ -71,6 +80,7 @@ class SlitherCompilationUnitSolc:
self._custom_error_parser: List[CustomErrorSolc] = []
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
@ -95,6 +105,10 @@ class SlitherCompilationUnitSolc:
def underlying_contract_to_parser(self) -> Dict[Contract, ContractSolc]:
return self._underlying_contract_to_parser
@property
def slither_parser(self) -> "SlitherCompilationUnitSolc":
return self
###################################################################################
###################################################################################
# region AST
@ -221,6 +235,17 @@ class SlitherCompilationUnitSolc:
scope.pragmas.add(pragma)
pragma.set_offset(top_level_data["src"], self._compilation_unit)
self._compilation_unit.pragma_directives.append(pragma)
elif top_level_data[self.get_key()] == "UsingForDirective":
scope = self.compilation_unit.get_scope(filename)
usingFor = UsingForTopLevel(scope)
usingFor_parser = UsingForTopLevelSolc(usingFor, top_level_data, self)
usingFor.set_offset(top_level_data["src"], self._compilation_unit)
scope.using_for_directives.add(usingFor)
self._compilation_unit.using_for_top_level.append(usingFor)
self._using_for_top_level_parser.append(usingFor_parser)
elif top_level_data[self.get_key()] == "ImportDirective":
if self.is_compact_ast:
import_directive = Import(
@ -491,6 +516,9 @@ Please rename it, this name is reserved for Slither's internals"""
# Then we analyse state variables, functions and modifiers
self._analyze_third_part(contracts_to_be_analyzed, libraries)
[c.set_is_analyzed(False) for c in self._underlying_contract_to_parser.values()]
self._analyze_using_for(contracts_to_be_analyzed, libraries)
self._parsed = True
@ -602,6 +630,29 @@ Please rename it, this name is reserved for Slither's internals"""
else:
contracts_to_be_analyzed += [contract]
def _analyze_using_for(
self, contracts_to_be_analyzed: List[ContractSolc], libraries: List[ContractSolc]
):
self._analyze_top_level_using_for()
for lib in libraries:
lib.analyze_using_for()
while contracts_to_be_analyzed:
contract = contracts_to_be_analyzed[0]
contracts_to_be_analyzed = contracts_to_be_analyzed[1:]
all_father_analyzed = all(
self._underlying_contract_to_parser[father].is_analyzed
for father in contract.underlying_contract.inheritance
)
if not contract.underlying_contract.inheritance or all_father_analyzed:
contract.analyze_using_for()
contract.set_is_analyzed(True)
else:
contracts_to_be_analyzed += [contract]
def _analyze_enums(self, contract: ContractSolc):
# Enum must be analyzed first
contract.analyze_enums()
@ -624,7 +675,6 @@ Please rename it, this name is reserved for Slither's internals"""
# Event can refer to struct
contract.analyze_events()
contract.analyze_using_for()
contract.analyze_custom_errors()
contract.set_is_analyzed(True)
@ -648,6 +698,10 @@ Please rename it, this name is reserved for Slither's internals"""
func_parser.analyze_params()
self._compilation_unit.add_function(func_parser.underlying_function)
def _analyze_top_level_using_for(self):
for using_for in self._using_for_top_level_parser:
using_for.analyze()
def _analyze_params_custom_error(self):
for custom_error_parser in self._custom_error_parser:
custom_error_parser.analyze_params()
@ -683,12 +737,13 @@ Please rename it, this name is reserved for Slither's internals"""
for func in contract.functions + contract.modifiers:
try:
func.generate_slithir_and_analyze()
except AttributeError as e:
# This can happens for example if there is a call to an interface
# And the interface is redefined due to contract's name reuse
# But the available version misses some functions
self._underlying_contract_to_parser[contract].log_incorrect_parsing(
f"Impossible to generate IR for {contract.name}.{func.name}:\n {e}"
f"Impossible to generate IR for {contract.name}.{func.name} ({func.source_mapping}):\n {e}"
)
contract.convert_expression_to_slithir_ssa()

@ -224,6 +224,7 @@ def parse_type(
from slither.solc_parsing.variables.function_type_variable import FunctionTypeVariableSolc
from slither.solc_parsing.declarations.contract import ContractSolc
from slither.solc_parsing.declarations.function import FunctionSolc
from slither.solc_parsing.declarations.using_for_top_level import UsingForTopLevelSolc
from slither.solc_parsing.declarations.custom_error import CustomErrorSolc
from slither.solc_parsing.declarations.structure_top_level import StructureTopLevelSolc
from slither.solc_parsing.slither_compilation_unit_solc import SlitherCompilationUnitSolc
@ -259,11 +260,16 @@ def parse_type(
all_enums += enums_direct_access
contracts = sl.contracts
functions = []
elif isinstance(caller_context, (StructureTopLevelSolc, CustomErrorSolc, TopLevelVariableSolc)):
elif isinstance(
caller_context,
(StructureTopLevelSolc, CustomErrorSolc, TopLevelVariableSolc, UsingForTopLevelSolc),
):
if isinstance(caller_context, StructureTopLevelSolc):
scope = caller_context.underlying_structure.file_scope
elif isinstance(caller_context, TopLevelVariableSolc):
scope = caller_context.underlying_variable.file_scope
elif isinstance(caller_context, UsingForTopLevelSolc):
scope = caller_context.underlying_using_for.file_scope
else:
assert isinstance(caller_context, CustomErrorSolc)
custom_error = caller_context.underlying_custom_error

@ -264,9 +264,9 @@ binary_ops = {
class YulBuiltin: # pylint: disable=too-few-public-methods
def __init__(self, name):
def __init__(self, name: str) -> None:
self._name = name
@property
def name(self):
def name(self) -> str:
return self._name

@ -24,6 +24,7 @@ from slither.core.expressions import (
UnaryOperation,
)
from slither.core.expressions.expression import Expression
from slither.core.scope.scope import FileScope
from slither.core.solidity_types import ElementaryType
from slither.core.source_mapping.source_mapping import SourceMapping
from slither.core.variables.local_variable import LocalVariable
@ -51,25 +52,30 @@ class YulNode:
def underlying_node(self) -> Node:
return self._node
def add_unparsed_expression(self, expression: Dict):
def add_unparsed_expression(self, expression: Dict) -> None:
assert self._unparsed_expression is None
self._unparsed_expression = expression
def analyze_expressions(self):
def analyze_expressions(self) -> None:
if self._node.type == NodeType.VARIABLE and not self._node.expression:
self._node.add_expression(self._node.variable_declaration.expression)
expression = self._node.variable_declaration.expression
if expression:
self._node.add_expression(expression)
if self._unparsed_expression:
expression = parse_yul(self._scope, self, self._unparsed_expression)
if expression:
self._node.add_expression(expression)
if self._node.expression:
if self._node.type == NodeType.VARIABLE:
# Update the expression to be an assignement to the variable
variable_declaration = self._node.variable_declaration
if variable_declaration:
_expression = AssignmentOperation(
Identifier(self._node.variable_declaration),
self._node.expression,
AssignmentOperationType.ASSIGN,
self._node.variable_declaration.type,
variable_declaration.type,
)
_expression.set_offset(
self._node.expression.source_mapping, self._node.compilation_unit
@ -122,13 +128,13 @@ class YulScope(metaclass=abc.ABCMeta):
]
def __init__(
self, contract: Optional[Contract], yul_id: List[str], parent_func: Function = None
):
self, contract: Optional[Contract], yul_id: List[str], parent_func: Function
) -> None:
self._contract = contract
self._id: List[str] = yul_id
self._yul_local_variables: List[YulLocalVariable] = []
self._yul_local_functions: List[YulFunction] = []
self._parent_func = parent_func
self._parent_func: Function = parent_func
@property
def id(self) -> List[str]:
@ -155,10 +161,14 @@ class YulScope(metaclass=abc.ABCMeta):
def new_node(self, node_type: NodeType, src: Union[str, Dict]) -> YulNode:
pass
def add_yul_local_variable(self, var):
@property
def file_scope(self) -> FileScope:
return self._parent_func.file_scope
def add_yul_local_variable(self, var: "YulLocalVariable") -> None:
self._yul_local_variables.append(var)
def get_yul_local_variable_from_name(self, variable_name):
def get_yul_local_variable_from_name(self, variable_name: str) -> Optional["YulLocalVariable"]:
return next(
(
v
@ -168,10 +178,10 @@ class YulScope(metaclass=abc.ABCMeta):
None,
)
def add_yul_local_function(self, func):
def add_yul_local_function(self, func: "YulFunction") -> None:
self._yul_local_functions.append(func)
def get_yul_local_function_from_name(self, func_name):
def get_yul_local_function_from_name(self, func_name: str) -> Optional["YulLocalVariable"]:
return next(
(v for v in self._yul_local_functions if v.underlying.name == func_name),
None,
@ -242,7 +252,7 @@ class YulFunction(YulScope):
def function(self) -> Function:
return self._function
def convert_body(self):
def convert_body(self) -> None:
node = self.new_node(NodeType.ENTRYPOINT, self._ast["src"])
link_underlying_nodes(self._entrypoint, node)
@ -258,7 +268,7 @@ class YulFunction(YulScope):
convert_yul(self, node, self._ast["body"], self.node_scope)
def parse_body(self):
def parse_body(self) -> None:
for node in self._nodes:
node.analyze_expressions()
@ -289,9 +299,8 @@ class YulBlock(YulScope):
entrypoint: Node,
yul_id: List[str],
node_scope: Union[Scope, Function],
**kwargs,
):
super().__init__(contract, yul_id, **kwargs)
super().__init__(contract, yul_id, entrypoint.function)
self._entrypoint: YulNode = YulNode(entrypoint, self)
self._nodes: List[YulNode] = []
@ -318,7 +327,7 @@ class YulBlock(YulScope):
def convert(self, ast: Dict) -> YulNode:
return convert_yul(self, self._entrypoint, ast, self.node_scope)
def analyze_expressions(self):
def analyze_expressions(self) -> None:
for node in self._nodes:
node.analyze_expressions()
@ -361,18 +370,22 @@ def convert_yul_function_definition(
while not isinstance(top_node_scope, Function):
top_node_scope = top_node_scope.father
func: Union[FunctionTopLevel, FunctionContract]
if isinstance(top_node_scope, FunctionTopLevel):
scope = root.contract.file_scope
scope = root.file_scope
func = FunctionTopLevel(root.compilation_unit, scope)
# Note: we do not add the function in the scope
# While its a top level function, it is not accessible outside of the function definition
# In practice we should probably have a specific function type for function defined within a function
else:
func = FunctionContract(root.compilation_unit)
func.function_language = FunctionLanguage.Yul
yul_function = YulFunction(func, root, ast, node_scope)
if root.contract:
root.contract.add_function(func)
root.compilation_unit.add_function(func)
root.add_yul_local_function(yul_function)
@ -774,14 +787,15 @@ def parse_yul_identifier(root: YulScope, _node: YulNode, ast: Dict) -> Optional[
# check function-scoped variables
parent_func = root.parent_func
if parent_func:
variable = parent_func.get_local_variable_from_name(name)
if variable:
return Identifier(variable)
local_variable = parent_func.get_local_variable_from_name(name)
if local_variable:
return Identifier(local_variable)
if isinstance(parent_func, FunctionContract):
variable = parent_func.contract.get_state_variable_from_name(name)
if variable:
return Identifier(variable)
assert parent_func.contract
state_variable = parent_func.contract.get_state_variable_from_name(name)
if state_variable:
return Identifier(state_variable)
# check yul-scoped variable
variable = root.get_yul_local_variable_from_name(name)
@ -798,7 +812,7 @@ def parse_yul_identifier(root: YulScope, _node: YulNode, ast: Dict) -> Optional[
if magic_suffix:
return magic_suffix
ret, _ = find_top_level(name, root.contract.file_scope)
ret, _ = find_top_level(name, root.file_scope)
if ret:
return Identifier(ret)
@ -840,7 +854,7 @@ def parse_yul_unsupported(_root: YulScope, _node: YulNode, ast: Dict) -> Optiona
def parse_yul(root: YulScope, node: YulNode, ast: Dict) -> Optional[Expression]:
op = parsers.get(ast["nodeType"], parse_yul_unsupported)(root, node, ast)
op: Expression = parsers.get(ast["nodeType"], parse_yul_unsupported)(root, node, ast)
if op:
op.set_offset(ast["src"], root.compilation_unit)
return op

@ -1,4 +1,6 @@
import argparse
import logging
import sys
from crytic_compile import cryticparser
@ -25,6 +27,9 @@ def parse_args() -> argparse.Namespace:
def main():
# log on stdout to keep output in order
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, force=True)
args = parse_args()
kwargs = vars(args)

@ -1,6 +1,7 @@
from typing import Callable, List
from dataclasses import dataclass
from slither.tools.doctor.checks.paths import check_slither_path
from slither.tools.doctor.checks.platform import compile_project, detect_platform
from slither.tools.doctor.checks.versions import show_versions
@ -12,6 +13,7 @@ class Check:
ALL_CHECKS: List[Check] = [
Check("PATH configuration", check_slither_path),
Check("Software versions", show_versions),
Check("Project platform", detect_platform),
Check("Project compilation", compile_project),

@ -0,0 +1,85 @@
from pathlib import Path
from typing import List, Optional, Tuple
import shutil
import sys
import sysconfig
from slither.utils.colors import yellow, green, red
def path_is_relative_to(path: Path, relative_to: Path) -> bool:
"""
Check if a path is relative to another one.
Compatibility wrapper for Path.is_relative_to
"""
if sys.version_info >= (3, 9, 0):
return path.is_relative_to(relative_to)
path_parts = path.resolve().parts
relative_to_parts = relative_to.resolve().parts
if len(path_parts) < len(relative_to_parts):
return False
for (a, b) in zip(path_parts, relative_to_parts):
if a != b:
return False
return True
def check_path_config(name: str) -> Tuple[bool, Optional[Path], List[Path]]:
"""
Check if a given Python binary/script is in PATH.
:return: Returns if the binary on PATH corresponds to this installation,
its path (if present), and a list of possible paths where this
binary might be found.
"""
binary_path = shutil.which(name)
possible_paths = []
for scheme in sysconfig.get_scheme_names():
script_path = Path(sysconfig.get_path("scripts", scheme))
purelib_path = Path(sysconfig.get_path("purelib", scheme))
script_binary_path = shutil.which(name, path=script_path)
if script_binary_path is not None:
possible_paths.append((script_path, purelib_path))
binary_here = False
if binary_path is not None:
binary_path = Path(binary_path)
this_code = Path(__file__)
this_binary = list(filter(lambda x: path_is_relative_to(this_code, x[1]), possible_paths))
binary_here = len(this_binary) > 0 and all(
path_is_relative_to(binary_path, script) for script, _ in this_binary
)
return binary_here, binary_path, list(set(script for script, _ in possible_paths))
def check_slither_path(**_kwargs) -> None:
binary_here, binary_path, possible_paths = check_path_config("slither")
show_paths = False
if binary_path:
print(green(f"`slither` found in PATH at `{binary_path}`."))
if binary_here:
print(green("Its location matches this slither-doctor installation."))
else:
print(
yellow(
"This path does not correspond to this slither-doctor installation.\n"
+ "Double-check the order of directories in PATH if you have several Slither installations."
)
)
show_paths = True
else:
print(red("`slither` was not found in PATH."))
show_paths = True
if show_paths:
print()
print("Consider adding one of the following directories to PATH:")
for path in possible_paths:
print(f" * {path}")

@ -3,19 +3,19 @@ import json
from typing import Optional
import urllib
from packaging.version import parse, LegacyVersion, Version
from packaging.version import parse, Version
from slither.utils.colors import yellow, green
def get_installed_version(name: str) -> Optional[LegacyVersion | Version]:
def get_installed_version(name: str) -> Optional[Version]:
try:
return parse(metadata.version(name))
except metadata.PackageNotFoundError:
return None
def get_github_version(name: str) -> Optional[LegacyVersion | Version]:
def get_github_version(name: str) -> Optional[Version]:
try:
with urllib.request.urlopen(
f"https://api.github.com/repos/crytic/{name}/releases/latest"
@ -45,7 +45,9 @@ def show_versions(**_kwargs) -> None:
for name, (installed, latest) in versions.items():
color = yellow if name in outdated else green
print(f"{name + ':':<16}{color(installed or 'N/A'):<16} (latest is {latest or 'Unknown'})")
print(
f"{name + ':':<16}{color(str(installed) or 'N/A'):<16} (latest is {str(latest) or 'Unknown'})"
)
if len(outdated) > 0:
print()

@ -0,0 +1,5 @@
# slither-documentation
`slither-documentation` uses [codex](https://beta.openai.com) to generate natspec documenation.
This tool is experimental. See [solmate documentation](https://github.com/montyly/solmate/pull/1) for an example of usage.

@ -0,0 +1,280 @@
import argparse
import logging
import uuid
from typing import Optional, Dict, List
from crytic_compile import cryticparser
from slither import Slither
from slither.core.compilation_unit import SlitherCompilationUnit
from slither.core.declarations import Function
from slither.formatters.utils.patches import create_patch, apply_patch, create_diff
from slither.utils import codex
logging.basicConfig()
logging.getLogger("Slither").setLevel(logging.INFO)
logger = logging.getLogger("Slither")
def parse_args() -> argparse.Namespace:
"""
Parse the underlying arguments for the program.
:return: Returns the arguments for the program.
"""
parser = argparse.ArgumentParser(description="Demo", usage="slither-documentation filename")
parser.add_argument("project", help="The target directory/Solidity file.")
parser.add_argument(
"--overwrite", help="Overwrite the files (be careful).", action="store_true", default=False
)
parser.add_argument(
"--force-answer-parsing",
help="Apply heuristics to better parse codex output (might lead to incorrect results)",
action="store_true",
default=False,
)
parser.add_argument(
"--retry",
help="Retry failed query (default 1). Each retry increases the temperature by 0.1",
action="store",
default=1,
)
# Add default arguments from crytic-compile
cryticparser.init(parser)
codex.init_parser(parser, always_enable_codex=True)
return parser.parse_args()
def _use_tab(char: str) -> Optional[bool]:
"""
Check if the char is a tab
Args:
char:
Returns:
"""
if char == " ":
return False
if char == "\t":
return True
return None
def _post_processesing(
answer: str, starting_column: int, use_tab: Optional[bool], force_and_stopped: bool
) -> Optional[str]:
"""
Clean answers from codex
Args:
answer:
starting_column:
Returns:
"""
if answer.count("/**") != 1:
return None
# Sometimes codex will miss the */, even if it finished properly the request
# In this case, we allow slither-documentation to force the */
if answer.count("*/") != 1:
if force_and_stopped:
answer += "*/"
else:
return None
if answer.find("/**") > answer.find("*/"):
return None
answer = answer[answer.find("/**") : answer.find("*/") + 2]
answer_lines = answer.splitlines()
# Add indentation to all the lines, aside the first one
space_char = "\t" if use_tab else " "
if len(answer_lines) > 0:
answer = (
answer_lines[0]
+ "\n"
+ "\n".join(
[space_char * (starting_column - 1) + line for line in answer_lines[1:] if line]
)
)
answer += "\n" + space_char * (starting_column - 1)
return answer
return answer_lines[0]
def _handle_codex(
answer: Dict, starting_column: int, use_tab: Optional[bool], force: bool
) -> Optional[str]:
if "choices" in answer:
if answer["choices"]:
if "text" in answer["choices"][0]:
has_stopped = answer["choices"][0].get("finish_reason", "") == "stop"
answer_processed = _post_processesing(
answer["choices"][0]["text"], starting_column, use_tab, force and has_stopped
)
if answer_processed is None:
return None
return answer_processed
return None
# pylint: disable=too-many-locals,too-many-arguments
def _handle_function(
function: Function,
overwrite: bool,
all_patches: Dict,
logging_file: Optional[str],
slither: Slither,
retry: int,
force: bool,
) -> bool:
if (
function.source_mapping.is_dependency
or function.has_documentation
or function.is_constructor_variables
):
return overwrite
prompt = "Create a natpsec documentation for this solidity code with only notice and dev.\n"
src_mapping = function.source_mapping
content = function.compilation_unit.core.source_code[src_mapping.filename.absolute]
start = src_mapping.start
end = src_mapping.start + src_mapping.length
prompt += content[start:end]
use_tab = _use_tab(content[start - 1])
if use_tab is None and src_mapping.starting_column > 1:
logger.info(f"Non standard space indentation found {content[start - 1:end]}")
if overwrite:
logger.info("Disable overwrite to avoid mistakes")
overwrite = False
openai = codex.openai_module() # type: ignore
if openai is None:
raise ImportError
if logging_file:
codex.log_codex(logging_file, "Q: " + prompt)
tentative = 0
answer_processed: Optional[str] = None
while tentative < retry:
tentative += 1
answer = openai.Completion.create( # type: ignore
prompt=prompt,
model=slither.codex_model,
temperature=min(slither.codex_temperature + tentative * 0.1, 1),
max_tokens=slither.codex_max_tokens,
)
if logging_file:
codex.log_codex(logging_file, "A: " + str(answer))
answer_processed = _handle_codex(answer, src_mapping.starting_column, use_tab, force)
if answer_processed:
break
logger.info(
f"Codex could not generate a well formatted answer for {function.canonical_name}"
)
logger.info(answer)
if not answer_processed:
return overwrite
create_patch(all_patches, src_mapping.filename.absolute, start, start, "", answer_processed)
return overwrite
def _handle_compilation_unit(
slither: Slither,
compilation_unit: SlitherCompilationUnit,
overwrite: bool,
force: bool,
retry: int,
) -> None:
logging_file: Optional[str]
if slither.codex_log:
logging_file = str(uuid.uuid4())
else:
logging_file = None
for scope in compilation_unit.scopes.values():
# Dont send tests file
if (
".t.sol" in scope.filename.absolute
or "mock" in scope.filename.absolute.lower()
or "test" in scope.filename.absolute.lower()
):
continue
functions_target: List[Function] = []
for contract in scope.contracts.values():
functions_target += contract.functions_declared
functions_target += list(scope.functions)
all_patches: Dict = {}
for function in functions_target:
overwrite = _handle_function(
function, overwrite, all_patches, logging_file, slither, retry, force
)
# all_patches["patches"] should have only 1 file
if "patches" not in all_patches:
continue
for file in all_patches["patches"]:
original_txt = compilation_unit.core.source_code[file].encode("utf8")
patched_txt = original_txt
patches = all_patches["patches"][file]
offset = 0
patches.sort(key=lambda x: x["start"])
for patch in patches:
patched_txt, offset = apply_patch(patched_txt, patch, offset)
if overwrite:
with open(file, "w", encoding="utf8") as f:
f.write(patched_txt.decode("utf8"))
else:
diff = create_diff(compilation_unit, original_txt, patched_txt, file)
with open(f"{file}.patch", "w", encoding="utf8") as f:
f.write(diff)
def main() -> None:
args = parse_args()
logger.info("This tool is a WIP, use it with cautious")
logger.info("Be aware of OpenAI ToS: https://openai.com/api/policies/terms/")
slither = Slither(args.project, **vars(args))
try:
for compilation_unit in slither.compilation_units:
_handle_compilation_unit(
slither,
compilation_unit,
args.overwrite,
args.force_answer_parsing,
int(args.retry),
)
except ImportError:
pass
if __name__ == "__main__":
main()

@ -9,7 +9,8 @@ from slither.detectors.attributes.incorrect_solc import IncorrectSolc
from slither.detectors.attributes.constant_pragma import ConstantPragma
from slither.detectors.naming_convention.naming_convention import NamingConvention
from slither.detectors.functions.external_function import ExternalFunction
from slither.detectors.variables.possible_const_state_variables import ConstCandidateStateVars
from slither.detectors.variables.could_be_constant import CouldBeConstant
from slither.detectors.variables.could_be_immutable import CouldBeImmutable
from slither.detectors.attributes.const_functions_asm import ConstantFunctionsAsm
from slither.detectors.attributes.const_functions_state import ConstantFunctionsState
from slither.utils.colors import yellow
@ -23,7 +24,8 @@ all_detectors: Dict[str, Type[AbstractDetector]] = {
"pragma": ConstantPragma,
"naming-convention": NamingConvention,
"external-function": ExternalFunction,
"constable-states": ConstCandidateStateVars,
"constable-states": CouldBeConstant,
"immutable-states": CouldBeImmutable,
"constant-function-asm": ConstantFunctionsAsm,
"constant-functions-state": ConstantFunctionsState,
}

@ -14,7 +14,10 @@ from slither.exceptions import SlitherException
from slither.utils.colors import red
from slither.utils.output import output_to_json
from slither.tools.upgradeability.checks import all_checks
from slither.tools.upgradeability.checks.abstract_checks import AbstractCheck
from slither.tools.upgradeability.checks.abstract_checks import (
AbstractCheck,
CheckClassification,
)
from slither.tools.upgradeability.utils.command_line import (
output_detectors_json,
output_wiki,
@ -27,12 +30,14 @@ logger: logging.Logger = logging.getLogger("Slither")
logger.setLevel(logging.INFO)
def parse_args() -> argparse.Namespace:
def parse_args(check_classes: List[Type[AbstractCheck]]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Slither Upgradeability Checks. For usage information see https://github.com/crytic/slither/wiki/Upgradeability-Checks.",
usage="slither-check-upgradeability contract.sol ContractName",
)
group_checks = parser.add_argument_group("Checks")
parser.add_argument("contract.sol", help="Codebase to analyze")
parser.add_argument("ContractName", help="Contract name (logic contract)")
@ -51,7 +56,16 @@ def parse_args() -> argparse.Namespace:
default=False,
)
parser.add_argument(
group_checks.add_argument(
"--detect",
help="Comma-separated list of detectors, defaults to all, "
f"available detectors: {', '.join(d.ARGUMENT for d in check_classes)}",
action="store",
dest="detectors_to_run",
default="all",
)
group_checks.add_argument(
"--list-detectors",
help="List available detectors",
action=ListDetectors,
@ -59,6 +73,42 @@ def parse_args() -> argparse.Namespace:
default=False,
)
group_checks.add_argument(
"--exclude",
help="Comma-separated list of detectors that should be excluded",
action="store",
dest="detectors_to_exclude",
default=None,
)
group_checks.add_argument(
"--exclude-informational",
help="Exclude informational impact analyses",
action="store_true",
default=False,
)
group_checks.add_argument(
"--exclude-low",
help="Exclude low impact analyses",
action="store_true",
default=False,
)
group_checks.add_argument(
"--exclude-medium",
help="Exclude medium impact analyses",
action="store_true",
default=False,
)
group_checks.add_argument(
"--exclude-high",
help="Exclude high impact analyses",
action="store_true",
default=False,
)
parser.add_argument(
"--markdown-root",
help="URL for markdown generation",
@ -104,6 +154,43 @@ def _get_checks() -> List[Type[AbstractCheck]]:
return detectors
def choose_checks(
args: argparse.Namespace, all_check_classes: List[Type[AbstractCheck]]
) -> List[Type[AbstractCheck]]:
detectors_to_run = []
detectors = {d.ARGUMENT: d for d in all_check_classes}
if args.detectors_to_run == "all":
detectors_to_run = all_check_classes
if args.detectors_to_exclude:
detectors_excluded = args.detectors_to_exclude.split(",")
for detector in detectors:
if detector in detectors_excluded:
detectors_to_run.remove(detectors[detector])
else:
for detector in args.detectors_to_run.split(","):
if detector in detectors:
detectors_to_run.append(detectors[detector])
else:
raise Exception(f"Error: {detector} is not a detector")
detectors_to_run = sorted(detectors_to_run, key=lambda x: x.IMPACT)
return detectors_to_run
if args.exclude_informational:
detectors_to_run = [
d for d in detectors_to_run if d.IMPACT != CheckClassification.INFORMATIONAL
]
if args.exclude_low:
detectors_to_run = [d for d in detectors_to_run if d.IMPACT != CheckClassification.LOW]
if args.exclude_medium:
detectors_to_run = [d for d in detectors_to_run if d.IMPACT != CheckClassification.MEDIUM]
if args.exclude_high:
detectors_to_run = [d for d in detectors_to_run if d.IMPACT != CheckClassification.HIGH]
# detectors_to_run = sorted(detectors_to_run, key=lambda x: x.IMPACT)
return detectors_to_run
class ListDetectors(argparse.Action): # pylint: disable=too-few-public-methods
def __call__(
self, parser: Any, *args: Any, **kwargs: Any
@ -200,11 +287,11 @@ def main() -> None:
"detectors": [],
}
args = parse_args()
detectors = _get_checks()
args = parse_args(detectors)
detectors_to_run = choose_checks(args, detectors)
v1_filename = vars(args)["contract.sol"]
number_detectors_run = 0
detectors = _get_checks()
try:
variable1 = Slither(v1_filename, **vars(args))
@ -219,7 +306,7 @@ def main() -> None:
return
v1_contract = v1_contracts[0]
detectors_results, number_detectors = _checks_on_contract(detectors, v1_contract)
detectors_results, number_detectors = _checks_on_contract(detectors_to_run, v1_contract)
json_results["detectors"] += detectors_results
number_detectors_run += number_detectors
@ -242,7 +329,7 @@ def main() -> None:
json_results["proxy-present"] = True
detectors_results, number_detectors = _checks_on_contract_and_proxy(
detectors, v1_contract, proxy_contract
detectors_to_run, v1_contract, proxy_contract
)
json_results["detectors"] += detectors_results
number_detectors_run += number_detectors
@ -267,19 +354,19 @@ def main() -> None:
if proxy_contract:
detectors_results, _ = _checks_on_contract_and_proxy(
detectors, v2_contract, proxy_contract
detectors_to_run, v2_contract, proxy_contract
)
json_results["detectors"] += detectors_results
detectors_results, number_detectors = _checks_on_contract_update(
detectors, v1_contract, v2_contract
detectors_to_run, v1_contract, v2_contract
)
json_results["detectors"] += detectors_results
number_detectors_run += number_detectors
# If there is a V2, we run the contract-only check on the V2
detectors_results, number_detectors = _checks_on_contract(detectors, v2_contract)
detectors_results, number_detectors = _checks_on_contract(detectors_to_run, v2_contract)
json_results["detectors"] += detectors_results
number_detectors_run += number_detectors

@ -39,8 +39,8 @@ Using initialize functions to write initial values in state variables.
def _check(self):
results = []
for s in self.contract.state_variables:
if s.initialized and not s.is_constant:
for s in self.contract.state_variables_ordered:
if s.initialized and not (s.is_constant or s.is_immutable):
info = [s, " is a state variable with an initial value.\n"]
json = self.generate_result(info)
results.append(json)

@ -48,8 +48,16 @@ Do not change the order of the state variables in the updated contract.
def _check(self):
contract1 = self.contract
contract2 = self.contract_v2
order1 = [variable for variable in contract1.state_variables if not variable.is_constant]
order2 = [variable for variable in contract2.state_variables if not variable.is_constant]
order1 = [
variable
for variable in contract1.state_variables_ordered
if not (variable.is_constant or variable.is_immutable)
]
order2 = [
variable
for variable in contract2.state_variables_ordered
if not (variable.is_constant or variable.is_immutable)
]
results = []
for idx, _ in enumerate(order1):
@ -109,8 +117,16 @@ Avoid variables in the proxy. If a variable is in the proxy, ensure it has the s
def _check(self):
contract1 = self._contract1()
contract2 = self._contract2()
order1 = [variable for variable in contract1.state_variables if not variable.is_constant]
order2 = [variable for variable in contract2.state_variables if not variable.is_constant]
order1 = [
variable
for variable in contract1.state_variables_ordered
if not (variable.is_constant or variable.is_immutable)
]
order2 = [
variable
for variable in contract2.state_variables_ordered
if not (variable.is_constant or variable.is_immutable)
]
results = []
for idx, _ in enumerate(order1):
@ -228,8 +244,16 @@ Avoid variables in the proxy. If a variable is in the proxy, ensure it has the s
def _check(self):
contract1 = self._contract1()
contract2 = self._contract2()
order1 = [variable for variable in contract1.state_variables if not variable.is_constant]
order2 = [variable for variable in contract2.state_variables if not variable.is_constant]
order1 = [
variable
for variable in contract1.state_variables_ordered
if not (variable.is_constant or variable.is_immutable)
]
order2 = [
variable
for variable in contract2.state_variables_ordered
if not (variable.is_constant or variable.is_immutable)
]
results = []

@ -1,10 +1,70 @@
import logging
import os
from argparse import ArgumentParser
from pathlib import Path
from slither.utils.command_line import defaults_flag_in_config
logger = logging.getLogger("Slither")
def init_parser(parser: ArgumentParser, always_enable_codex: bool = False) -> None:
"""
Init the cli arg with codex features
Args:
parser:
always_enable_codex (Optional(bool)): if true, --codex is not enabled
Returns:
"""
group_codex = parser.add_argument_group("Codex (https://beta.openai.com/docs/guides/code)")
if not always_enable_codex:
group_codex.add_argument(
"--codex",
help="Enable codex (require an OpenAI API Key)",
action="store_true",
default=defaults_flag_in_config["codex"],
)
group_codex.add_argument(
"--codex-log",
help="Log codex queries (in crytic_export/codex/)",
action="store_true",
default=False,
)
group_codex.add_argument(
"--codex-contracts",
help="Comma separated list of contracts to submit to OpenAI Codex",
action="store",
default=defaults_flag_in_config["codex_contracts"],
)
group_codex.add_argument(
"--codex-model",
help="Name of the Codex model to use (affects pricing). Defaults to 'text-davinci-003'",
action="store",
default=defaults_flag_in_config["codex_model"],
)
group_codex.add_argument(
"--codex-temperature",
help="Temperature to use with Codex. Lower number indicates a more precise answer while higher numbers return more creative answers. Defaults to 0",
action="store",
default=defaults_flag_in_config["codex_temperature"],
)
group_codex.add_argument(
"--codex-max-tokens",
help="Maximum amount of tokens to use on the response. This number plus the size of the prompt can be no larger than the limit (4097 for text-davinci-003)",
action="store",
default=defaults_flag_in_config["codex_max_tokens"],
)
# TODO: investigate how to set the correct return type
# So that the other modules can work with openai
def openai_module(): # type: ignore

@ -60,6 +60,7 @@ defaults_flag_in_config = {
"zip": None,
"zip_type": "lzma",
"show_ignored_findings": False,
"no_fail": False,
**DEFAULTS_FLAG_IN_CONFIG_CRYTIC_COMPILE,
}

@ -23,20 +23,29 @@ from slither.all_exceptions import SlitherException
# pylint: disable=protected-access
def f_expressions(
e: AssignmentOperation, x: Union[Identifier, Literal, MemberAccess, IndexAccess]
e: Union[AssignmentOperation, BinaryOperation, TupleExpression],
x: Union[Identifier, Literal, MemberAccess, IndexAccess],
) -> None:
e._expressions.append(x)
def f_call(e, x):
def f_call(e: CallExpression, x):
e._arguments.append(x)
def f_expression(e, x):
def f_call_value(e: CallExpression, x):
e._value = x
def f_call_gas(e: CallExpression, x):
e._gas = x
def f_expression(e: Union[TypeConversion, UnaryOperation, MemberAccess], x):
e._expression = x
def f_called(e, x):
def f_called(e: CallExpression, x):
e._called = x
@ -53,13 +62,20 @@ class SplitTernaryExpression:
self.condition = None
self.copy_expression(expression, self.true_expression, self.false_expression)
def apply_copy(
def conditional_not_ahead(
self,
next_expr: Expression,
true_expression: Union[AssignmentOperation, MemberAccess],
false_expression: Union[AssignmentOperation, MemberAccess],
f: Callable,
) -> bool:
# look ahead for parenthetical expression (.. ? .. : ..)
if (
isinstance(next_expr, TupleExpression)
and len(next_expr.expressions) == 1
and isinstance(next_expr.expressions[0], ConditionalExpression)
):
next_expr = next_expr.expressions[0]
if isinstance(next_expr, ConditionalExpression):
f(true_expression, copy.copy(next_expr.then_expression))
@ -71,7 +87,6 @@ class SplitTernaryExpression:
f(false_expression, copy.copy(next_expr))
return True
# pylint: disable=too-many-branches
def copy_expression(
self, expression: Expression, true_expression: Expression, false_expression: Expression
) -> None:
@ -87,27 +102,47 @@ class SplitTernaryExpression:
):
return
# case of lib
# (.. ? .. : ..).add
if isinstance(expression, MemberAccess):
if isinstance(expression, (AssignmentOperation, BinaryOperation, TupleExpression)):
true_expression._expressions = []
false_expression._expressions = []
self.convert_expressions(expression, true_expression, false_expression)
elif isinstance(expression, CallExpression):
next_expr = expression.called
self.convert_call_expression(expression, next_expr, true_expression, false_expression)
elif isinstance(expression, (TypeConversion, UnaryOperation, MemberAccess)):
next_expr = expression.expression
if self.apply_copy(next_expr, true_expression, false_expression, f_expression):
if self.conditional_not_ahead(
next_expr, true_expression, false_expression, f_expression
):
self.copy_expression(
next_expr, true_expression.expression, false_expression.expression
expression.expression,
true_expression.expression,
false_expression.expression,
)
elif isinstance(expression, (AssignmentOperation, BinaryOperation, TupleExpression)):
true_expression._expressions = []
false_expression._expressions = []
else:
raise SlitherException(
f"Ternary operation not handled {expression}({type(expression)})"
)
def convert_expressions(
self,
expression: Union[AssignmentOperation, BinaryOperation, TupleExpression],
true_expression: Expression,
false_expression: Expression,
) -> None:
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 isinstance(next_expr, IndexAccess):
# create an index access for each branch
if isinstance(next_expr.expression_right, ConditionalExpression):
next_expr = _handle_ternary_access(
next_expr, true_expression, false_expression
)
if self.apply_copy(next_expr, true_expression, false_expression, f_expressions):
self.convert_index_access(next_expr, true_expression, false_expression)
if self.conditional_not_ahead(
next_expr, true_expression, false_expression, f_expressions
):
# always on last arguments added
self.copy_expression(
next_expr,
@ -115,68 +150,67 @@ class SplitTernaryExpression:
false_expression.expressions[-1],
)
elif isinstance(expression, CallExpression):
next_expr = expression.called
def convert_index_access(
self, next_expr: IndexAccess, true_expression: Expression, false_expression: Expression
) -> None:
# create an index access for each branch
# x[if cond ? 1 : 2] -> if cond { x[1] } else { x[2] }
for expr in next_expr.expressions:
if self.conditional_not_ahead(expr, true_expression, false_expression, f_expressions):
self.copy_expression(
expr,
true_expression.expressions[-1],
false_expression.expressions[-1],
)
def convert_call_expression(
self,
expression: CallExpression,
next_expr: Expression,
true_expression: Expression,
false_expression: Expression,
) -> None:
# case of lib
# (.. ? .. : ..).add
if self.apply_copy(next_expr, true_expression, false_expression, f_called):
if self.conditional_not_ahead(next_expr, true_expression, false_expression, f_called):
self.copy_expression(next_expr, true_expression.called, false_expression.called)
true_expression._arguments = []
false_expression._arguments = []
for next_expr in expression.arguments:
if self.apply_copy(next_expr, true_expression, false_expression, f_call):
# always on last arguments added
# In order to handle ternaries in both call options, gas and value, we return early if the
# conditional is not ahead to rewrite both ternaries (see `_rewrite_ternary_as_if_else`).
if expression.call_gas:
# case of (..).func{gas: .. ? .. : ..}()
next_expr = expression.call_gas
if self.conditional_not_ahead(next_expr, true_expression, false_expression, f_call_gas):
self.copy_expression(
next_expr,
true_expression.arguments[-1],
false_expression.arguments[-1],
true_expression.call_gas,
false_expression.call_gas,
)
else:
return
elif isinstance(expression, (TypeConversion, UnaryOperation)):
next_expr = expression.expression
if self.apply_copy(next_expr, true_expression, false_expression, f_expression):
if expression.call_value:
# case of (..).func{value: .. ? .. : ..}()
next_expr = expression.call_value
if self.conditional_not_ahead(
next_expr, true_expression, false_expression, f_call_value
):
self.copy_expression(
expression.expression,
true_expression.expression,
false_expression.expression,
next_expr,
true_expression.call_value,
false_expression.call_value,
)
else:
raise SlitherException(
f"Ternary operation not handled {expression}({type(expression)})"
)
return
def _handle_ternary_access(
next_expr: IndexAccess,
true_expression: AssignmentOperation,
false_expression: AssignmentOperation,
):
"""
Conditional ternary accesses are split into two accesses, one true and one false
E.g. x[if cond ? 1 : 2] -> if cond { x[1] } else { x[2] }
"""
true_index_access = IndexAccess(
next_expr.expression_left,
next_expr.expression_right.then_expression,
next_expr.type,
)
false_index_access = IndexAccess(
next_expr.expression_left,
next_expr.expression_right.else_expression,
next_expr.type,
)
true_expression._arguments = []
false_expression._arguments = []
f_expressions(
true_expression,
true_index_access,
)
f_expressions(
false_expression,
false_index_access,
for expr in expression.arguments:
if self.conditional_not_ahead(expr, true_expression, false_expression, f_call):
# always on last arguments added
self.copy_expression(
expr,
true_expression.arguments[-1],
false_expression.arguments[-1],
)
return next_expr.expression_right

@ -1,4 +1,12 @@
from slither.core.expressions import BinaryOperationType, Literal, UnaryOperationType
from fractions import Fraction
from slither.core.expressions import (
BinaryOperationType,
Literal,
UnaryOperationType,
Identifier,
BinaryOperation,
UnaryOperation,
)
from slither.utils.integer_conversion import convert_string_to_fraction, convert_string_to_int
from slither.visitors.expression.expression import ExpressionVisitor
@ -27,9 +35,15 @@ class ConstantFolding(ExpressionVisitor):
super().__init__(expression)
def result(self):
return Literal(int(get_val(self._expression)), self._type)
def _post_identifier(self, expression):
value = get_val(self._expression)
if isinstance(value, Fraction):
value = int(value)
# emulate 256-bit wrapping
if str(self._type).startswith("uint"):
value = value & (2**256 - 1)
return Literal(value, self._type)
def _post_identifier(self, expression: Identifier):
if not expression.value.is_constant:
raise NotConstant
expr = expression.value.expression
@ -37,9 +51,10 @@ class ConstantFolding(ExpressionVisitor):
if not isinstance(expr, Literal):
cf = ConstantFolding(expr, self._type)
expr = cf.result()
set_val(expression, convert_string_to_int(expr.value))
set_val(expression, convert_string_to_int(expr.converted_value))
def _post_binary_operation(self, expression):
# pylint: disable=too-many-branches
def _post_binary_operation(self, expression: BinaryOperation):
left = get_val(expression.expression_left)
right = get_val(expression.expression_right)
if expression.type == BinaryOperationType.POWER:
@ -53,18 +68,39 @@ class ConstantFolding(ExpressionVisitor):
elif expression.type == BinaryOperationType.ADDITION:
set_val(expression, left + right)
elif expression.type == BinaryOperationType.SUBTRACTION:
if (left - right) < 0:
# Could trigger underflow
raise NotConstant
set_val(expression, left - right)
# Convert to int for operations not supported by Fraction
elif expression.type == BinaryOperationType.LEFT_SHIFT:
set_val(expression, left << right)
set_val(expression, int(left) << int(right))
elif expression.type == BinaryOperationType.RIGHT_SHIFT:
set_val(expression, left >> right)
set_val(expression, int(left) >> int(right))
elif expression.type == BinaryOperationType.AND:
set_val(expression, int(left) & int(right))
elif expression.type == BinaryOperationType.CARET:
set_val(expression, int(left) ^ int(right))
elif expression.type == BinaryOperationType.OR:
set_val(expression, int(left) | int(right))
elif expression.type == BinaryOperationType.LESS:
set_val(expression, int(left) < int(right))
elif expression.type == BinaryOperationType.LESS_EQUAL:
set_val(expression, int(left) <= int(right))
elif expression.type == BinaryOperationType.GREATER:
set_val(expression, int(left) > int(right))
elif expression.type == BinaryOperationType.GREATER_EQUAL:
set_val(expression, int(left) >= int(right))
elif expression.type == BinaryOperationType.EQUAL:
set_val(expression, int(left) == int(right))
elif expression.type == BinaryOperationType.NOT_EQUAL:
set_val(expression, int(left) != int(right))
# Convert boolean literals from string to bool
elif expression.type == BinaryOperationType.ANDAND:
set_val(expression, left == "true" and right == "true")
elif expression.type == BinaryOperationType.OROR:
set_val(expression, left == "true" or right == "true")
else:
raise NotConstant
def _post_unary_operation(self, expression):
def _post_unary_operation(self, expression: UnaryOperation):
# Case of uint a = -7; uint[-a] arr;
if expression.type == UnaryOperationType.MINUS_PRE:
expr = expression.expression
@ -72,13 +108,16 @@ class ConstantFolding(ExpressionVisitor):
cf = ConstantFolding(expr, self._type)
expr = cf.result()
assert isinstance(expr, Literal)
set_val(expression, -convert_string_to_fraction(expr.value))
set_val(expression, -convert_string_to_fraction(expr.converted_value))
else:
raise NotConstant
def _post_literal(self, expression):
def _post_literal(self, expression: Literal):
if expression.converted_value in ["true", "false"]:
set_val(expression, expression.converted_value)
else:
try:
set_val(expression, convert_string_to_fraction(expression.value))
set_val(expression, convert_string_to_fraction(expression.converted_value))
except ValueError as e:
raise NotConstant from e
@ -115,9 +154,12 @@ class ConstantFolding(ExpressionVisitor):
cf = ConstantFolding(expression.expressions[0], self._type)
expr = cf.result()
assert isinstance(expr, Literal)
set_val(expression, convert_string_to_fraction(expr.value))
set_val(expression, convert_string_to_fraction(expr.converted_value))
return
raise NotConstant
def _post_type_conversion(self, expression):
raise NotConstant
cf = ConstantFolding(expression.expression, self._type)
expr = cf.result()
assert isinstance(expr, Literal)
set_val(expression, convert_string_to_fraction(expr.converted_value))

@ -282,10 +282,15 @@ class ExpressionToSlithIR(ExpressionVisitor):
and expression_called.member_name in ["wrap", "unwrap"]
and len(args) == 1
):
# wrap: underlying_type -> alias
# unwrap: alias -> underlying_type
dest_type = (
called if expression_called.member_name == "wrap" else called.underlying_type
)
val = TemporaryVariable(self._node)
var = TypeConversion(val, args[0], called)
var = TypeConversion(val, args[0], dest_type)
var.set_expression(expression)
val.set_type(called)
val.set_type(dest_type)
self._result.append(var)
set_val(expression, val)
@ -455,14 +460,18 @@ class ExpressionToSlithIR(ExpressionVisitor):
set_val(expression, expr)
return
if isinstance(expr, Contract):
# Early lookup to detect user defined types from other contracts definitions
# 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 isinstance(expr, Contract):
if expression.member_name in expr.file_scope.user_defined_types:
set_val(expression, expr.file_scope.user_defined_types[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
val = ReferenceVariable(self._node)
member = Member(expr, Constant(expression.member_name), val)

@ -0,0 +1,9 @@
pragma solidity 0.5.12;
import {A} from "./import.sol";
contract Z is A {
function test() public pure returns (uint) {
return 1;
}
}

@ -0,0 +1,9 @@
pragma solidity 0.5.12;
import {A as X, A as Y} from "./import.sol";
contract Z is X {
function test() public pure returns (uint) {
return 1;
}
}

@ -0,0 +1,13 @@
contract Test {
error myError();
}
interface VM {
function expectRevert(bytes4) external;
function expectRevert(bytes calldata) external;
}
contract A {
function b(address c) public {
VM(c).expectRevert(Test.myError.selector);
}
}

@ -0,0 +1,6 @@
{
"A": {},
"Z": {
"test()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: RETURN 1\n\"];\n}\n"
}
}

@ -0,0 +1,6 @@
{
"A": {},
"Z": {
"test()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: RETURN 1\n\"];\n}\n"
}
}

@ -0,0 +1,10 @@
{
"Test": {},
"VM": {
"expectRevert(bytes4)": "digraph{\n}\n",
"expectRevert(bytes)": "digraph{\n}\n"
},
"A": {
"b(address)": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n"
}
}

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

Loading…
Cancel
Save