mirror of https://github.com/crytic/slither
commit
3ac88be839
@ -0,0 +1,8 @@ |
||||
--- |
||||
version: 2 |
||||
updates: |
||||
- package-ecosystem: "github-actions" |
||||
directory: "/" |
||||
target-branch: "dev" |
||||
schedule: |
||||
interval: "weekly" |
@ -0,0 +1,54 @@ |
||||
name: Publish to PyPI |
||||
|
||||
on: |
||||
release: |
||||
types: [published] |
||||
|
||||
jobs: |
||||
build-release: |
||||
|
||||
runs-on: ubuntu-latest |
||||
|
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
|
||||
- name: Set up Python |
||||
uses: actions/setup-python@v4 |
||||
with: |
||||
python-version: '3.x' |
||||
|
||||
- name: Build distributions |
||||
run: | |
||||
python -m pip install --upgrade pip |
||||
python -m pip install build |
||||
python -m build |
||||
- name: Upload distributions |
||||
uses: actions/upload-artifact@v3 |
||||
with: |
||||
name: slither-dists |
||||
path: dist/ |
||||
|
||||
publish: |
||||
runs-on: ubuntu-latest |
||||
environment: release |
||||
permissions: |
||||
id-token: write # For trusted publishing + codesigning. |
||||
contents: write # For attaching signing artifacts to the release. |
||||
needs: |
||||
- build-release |
||||
steps: |
||||
- name: fetch dists |
||||
uses: actions/download-artifact@v3 |
||||
with: |
||||
name: slither-dists |
||||
path: dist/ |
||||
|
||||
- name: publish |
||||
uses: pypa/gh-action-pypi-publish@v1.8.8 |
||||
|
||||
- name: sign |
||||
uses: sigstore/gh-action-sigstore-python@v1.2.3 |
||||
with: |
||||
inputs: ./dist/*.tar.gz ./dist/*.whl |
||||
release-signing-artifacts: true |
||||
bundle-only: true |
@ -0,0 +1,88 @@ |
||||
SHELL := /bin/bash
|
||||
|
||||
PY_MODULE := slither
|
||||
TEST_MODULE := tests
|
||||
|
||||
ALL_PY_SRCS := $(shell find $(PY_MODULE) -name '*.py') \
|
||||
$(shell find test -name '*.py')
|
||||
|
||||
# Optionally overriden by the user, if they're using a virtual environment manager.
|
||||
VENV ?= env
|
||||
|
||||
# On Windows, venv scripts/shims are under `Scripts` instead of `bin`.
|
||||
VENV_BIN := $(VENV)/bin
|
||||
ifeq ($(OS),Windows_NT) |
||||
VENV_BIN := $(VENV)/Scripts
|
||||
endif |
||||
|
||||
# Optionally overridden by the user in the `release` target.
|
||||
BUMP_ARGS :=
|
||||
|
||||
# Optionally overridden by the user in the `test` target.
|
||||
TESTS :=
|
||||
|
||||
# Optionally overridden by the user/CI, to limit the installation to a specific
|
||||
# subset of development dependencies.
|
||||
SLITHER_EXTRA := dev
|
||||
|
||||
# If the user selects a specific test pattern to run, set `pytest` to fail fast
|
||||
# and only run tests that match the pattern.
|
||||
# Otherwise, run all tests and enable coverage assertions, since we expect
|
||||
# complete test coverage.
|
||||
ifneq ($(TESTS),) |
||||
TEST_ARGS := -x -k $(TESTS)
|
||||
COV_ARGS :=
|
||||
else |
||||
TEST_ARGS := -n auto
|
||||
COV_ARGS := # --fail-under 100
|
||||
endif |
||||
|
||||
.PHONY: all |
||||
all: |
||||
@echo "Run my targets individually!"
|
||||
|
||||
.PHONY: dev |
||||
dev: $(VENV)/pyvenv.cfg |
||||
|
||||
.PHONY: run |
||||
run: $(VENV)/pyvenv.cfg |
||||
@. $(VENV_BIN)/activate && slither $(ARGS)
|
||||
|
||||
$(VENV)/pyvenv.cfg: pyproject.toml |
||||
# Create our Python 3 virtual environment
|
||||
python3 -m venv env
|
||||
$(VENV_BIN)/python -m pip install --upgrade pip
|
||||
$(VENV_BIN)/python -m pip install -e .[$(SLITHER_EXTRA)]
|
||||
|
||||
.PHONY: lint |
||||
lint: $(VENV)/pyvenv.cfg |
||||
. $(VENV_BIN)/activate && \
|
||||
black --check . && \
|
||||
pylint $(PY_MODULE) $(TEST_MODULE)
|
||||
# ruff $(ALL_PY_SRCS) && \
|
||||
# mypy $(PY_MODULE) &&
|
||||
|
||||
.PHONY: reformat |
||||
reformat: |
||||
. $(VENV_BIN)/activate && \
|
||||
black .
|
||||
|
||||
.PHONY: test tests |
||||
test tests: $(VENV)/pyvenv.cfg |
||||
. $(VENV_BIN)/activate && \
|
||||
pytest --cov=$(PY_MODULE) $(T) $(TEST_ARGS) && \
|
||||
python -m coverage report -m $(COV_ARGS)
|
||||
|
||||
.PHONY: doc |
||||
doc: $(VENV)/pyvenv.cfg |
||||
. $(VENV_BIN)/activate && \
|
||||
PDOC_ALLOW_EXEC=1 pdoc -o html slither '!slither.tools'
|
||||
|
||||
.PHONY: package |
||||
package: $(VENV)/pyvenv.cfg |
||||
. $(VENV_BIN)/activate && \
|
||||
python3 -m build
|
||||
|
||||
.PHONY: edit |
||||
edit: |
||||
$(EDITOR) $(ALL_PY_SRCS)
|
@ -1,3 +1,9 @@ |
||||
contract A{ |
||||
pragma solidity 0.8.19; |
||||
|
||||
error RevertIt(); |
||||
|
||||
contract Example { |
||||
function reverts() external pure { |
||||
revert RevertIt(); |
||||
} |
||||
} |
@ -1,5 +1,16 @@ |
||||
import "./a.sol"; |
||||
|
||||
contract B is A{ |
||||
pragma solidity 0.8.19; |
||||
|
||||
enum B { |
||||
a, |
||||
b |
||||
} |
||||
|
||||
contract T { |
||||
Example e = new Example(); |
||||
function b() public returns(uint) { |
||||
B b = B.a; |
||||
return 4; |
||||
} |
||||
} |
@ -0,0 +1,95 @@ |
||||
#!/usr/bin/env bash |
||||
|
||||
### Test slither-interface |
||||
|
||||
DIR_TESTS="tests/tools/interface" |
||||
|
||||
solc-select use 0.8.19 --always-install |
||||
|
||||
#Test 1 - Etherscan target |
||||
slither-interface WETH9 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 |
||||
DIFF=$(diff crytic-export/interfaces/IWETH9.sol "$DIR_TESTS/test_1.sol" --strip-trailing-cr) |
||||
if [ "$DIFF" != "" ] |
||||
then |
||||
echo "slither-interface test 1 failed" |
||||
cat "crytic-export/interfaces/IWETH9.sol" |
||||
echo "" |
||||
cat "$DIR_TESTS/test_1.sol" |
||||
exit 255 |
||||
fi |
||||
|
||||
|
||||
#Test 2 - Local file target |
||||
slither-interface Mock tests/tools/interface/ContractMock.sol |
||||
DIFF=$(diff crytic-export/interfaces/IMock.sol "$DIR_TESTS/test_2.sol" --strip-trailing-cr) |
||||
if [ "$DIFF" != "" ] |
||||
then |
||||
echo "slither-interface test 2 failed" |
||||
cat "crytic-export/interfaces/IMock.sol" |
||||
echo "" |
||||
cat "$DIR_TESTS/test_2.sol" |
||||
exit 255 |
||||
fi |
||||
|
||||
|
||||
#Test 3 - unroll structs |
||||
slither-interface Mock tests/tools/interface/ContractMock.sol --unroll-structs |
||||
DIFF=$(diff crytic-export/interfaces/IMock.sol "$DIR_TESTS/test_3.sol" --strip-trailing-cr) |
||||
if [ "$DIFF" != "" ] |
||||
then |
||||
echo "slither-interface test 3 failed" |
||||
cat "crytic-export/interfaces/IMock.sol" |
||||
echo "" |
||||
cat "$DIR_TESTS/test_3.sol" |
||||
exit 255 |
||||
fi |
||||
|
||||
#Test 4 - exclude structs |
||||
slither-interface Mock tests/tools/interface/ContractMock.sol --exclude-structs |
||||
DIFF=$(diff crytic-export/interfaces/IMock.sol "$DIR_TESTS/test_4.sol" --strip-trailing-cr) |
||||
if [ "$DIFF" != "" ] |
||||
then |
||||
echo "slither-interface test 4 failed" |
||||
cat "crytic-export/interfaces/IMock.sol" |
||||
echo "" |
||||
cat "$DIR_TESTS/test_4.sol" |
||||
exit 255 |
||||
fi |
||||
|
||||
#Test 5 - exclude errors |
||||
slither-interface Mock tests/tools/interface/ContractMock.sol --exclude-errors |
||||
DIFF=$(diff crytic-export/interfaces/IMock.sol "$DIR_TESTS/test_5.sol" --strip-trailing-cr) |
||||
if [ "$DIFF" != "" ] |
||||
then |
||||
echo "slither-interface test 5 failed" |
||||
cat "crytic-export/interfaces/IMock.sol" |
||||
echo "" |
||||
cat "$DIR_TESTS/test_5.sol" |
||||
exit 255 |
||||
fi |
||||
|
||||
#Test 6 - exclude enums |
||||
slither-interface Mock tests/tools/interface/ContractMock.sol --exclude-enums |
||||
DIFF=$(diff crytic-export/interfaces/IMock.sol "$DIR_TESTS/test_6.sol" --strip-trailing-cr) |
||||
if [ "$DIFF" != "" ] |
||||
then |
||||
echo "slither-interface test 6 failed" |
||||
cat "crytic-export/interfaces/IMock.sol" |
||||
echo "" |
||||
cat "$DIR_TESTS/test_6.sol" |
||||
exit 255 |
||||
fi |
||||
|
||||
#Test 7 - exclude events |
||||
slither-interface Mock tests/tools/interface/ContractMock.sol --exclude-events |
||||
DIFF=$(diff crytic-export/interfaces/IMock.sol "$DIR_TESTS/test_7.sol" --strip-trailing-cr) |
||||
if [ "$DIFF" != "" ] |
||||
then |
||||
echo "slither-interface test 7 failed" |
||||
cat "crytic-export/interfaces/IMock.sol" |
||||
echo "" |
||||
cat "$DIR_TESTS/test_7.sol" |
||||
exit 255 |
||||
fi |
||||
|
||||
rm -r crytic-export |
@ -1 +1,4 @@ |
||||
""" |
||||
.. include:: ../README.md |
||||
""" |
||||
from .slither import Slither |
||||
|
@ -1,32 +1,23 @@ |
||||
from typing import Union, TYPE_CHECKING |
||||
|
||||
from typing import TYPE_CHECKING |
||||
|
||||
from slither.core.expressions.expression import Expression |
||||
from slither.core.solidity_types.type import Type |
||||
|
||||
if TYPE_CHECKING: |
||||
from slither.core.solidity_types.elementary_type import ElementaryType |
||||
from slither.core.solidity_types.type_alias import TypeAliasTopLevel |
||||
from slither.core.solidity_types.array_type import ArrayType |
||||
|
||||
|
||||
class NewArray(Expression): |
||||
|
||||
# note: dont conserve the size of the array if provided |
||||
def __init__( |
||||
self, depth: int, array_type: Union["TypeAliasTopLevel", "ElementaryType"] |
||||
) -> None: |
||||
def __init__(self, array_type: "ArrayType") -> None: |
||||
super().__init__() |
||||
assert isinstance(array_type, Type) |
||||
self._depth: int = depth |
||||
self._array_type: Type = array_type |
||||
# pylint: disable=import-outside-toplevel |
||||
from slither.core.solidity_types.array_type import ArrayType |
||||
|
||||
@property |
||||
def array_type(self) -> Type: |
||||
return self._array_type |
||||
assert isinstance(array_type, ArrayType) |
||||
self._array_type = array_type |
||||
|
||||
@property |
||||
def depth(self) -> int: |
||||
return self._depth |
||||
def array_type(self) -> "ArrayType": |
||||
return self._array_type |
||||
|
||||
def __str__(self): |
||||
return "new " + str(self._array_type) + "[]" * self._depth |
||||
return "new " + str(self._array_type) |
||||
|
@ -0,0 +1,226 @@ |
||||
from typing import List, Set |
||||
|
||||
from slither.core.cfg.node import Node, NodeType |
||||
from slither.core.declarations import Function |
||||
from slither.core.expressions import BinaryOperation, Identifier, MemberAccess, UnaryOperation |
||||
from slither.core.solidity_types import ArrayType |
||||
from slither.core.variables import StateVariable |
||||
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification |
||||
from slither.slithir.operations import Length, Delete, HighLevelCall |
||||
|
||||
|
||||
class CacheArrayLength(AbstractDetector): |
||||
""" |
||||
Detects `for` loops that use `length` member of some storage array in their loop condition and don't modify it. |
||||
""" |
||||
|
||||
ARGUMENT = "cache-array-length" |
||||
HELP = ( |
||||
"Detects `for` loops that use `length` member of some storage array in their loop condition and don't " |
||||
"modify it. " |
||||
) |
||||
IMPACT = DetectorClassification.OPTIMIZATION |
||||
CONFIDENCE = DetectorClassification.HIGH |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#cache-array-length" |
||||
|
||||
WIKI_TITLE = "Cache array length" |
||||
WIKI_DESCRIPTION = ( |
||||
"Detects `for` loops that use `length` member of some storage array in their loop condition " |
||||
"and don't modify it. " |
||||
) |
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
contract C |
||||
{ |
||||
uint[] array; |
||||
|
||||
function f() public |
||||
{ |
||||
for (uint i = 0; i < array.length; i++) |
||||
{ |
||||
// code that does not modify length of `array` |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
Since the `for` loop in `f` doesn't modify `array.length`, it is more gas efficient to cache it in some local variable and use that variable instead, like in the following example: |
||||
|
||||
```solidity |
||||
contract C |
||||
{ |
||||
uint[] array; |
||||
|
||||
function f() public |
||||
{ |
||||
uint array_length = array.length; |
||||
for (uint i = 0; i < array_length; i++) |
||||
{ |
||||
// code that does not modify length of `array` |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
""" |
||||
WIKI_RECOMMENDATION = ( |
||||
"Cache the lengths of storage arrays if they are used and not modified in `for` loops." |
||||
) |
||||
|
||||
@staticmethod |
||||
def _is_identifier_member_access_comparison(exp: BinaryOperation) -> bool: |
||||
""" |
||||
Checks whether a BinaryOperation `exp` is an operation on Identifier and MemberAccess. |
||||
""" |
||||
return ( |
||||
isinstance(exp.expression_left, Identifier) |
||||
and isinstance(exp.expression_right, MemberAccess) |
||||
) or ( |
||||
isinstance(exp.expression_left, MemberAccess) |
||||
and isinstance(exp.expression_right, Identifier) |
||||
) |
||||
|
||||
@staticmethod |
||||
def _extract_array_from_length_member_access(exp: MemberAccess) -> StateVariable: |
||||
""" |
||||
Given a member access `exp`, it returns state array which `length` member is accessed through `exp`. |
||||
If array is not a state array or its `length` member is not referenced, it returns `None`. |
||||
""" |
||||
if exp.member_name != "length": |
||||
return None |
||||
if not isinstance(exp.expression, Identifier): |
||||
return None |
||||
if not isinstance(exp.expression.value, StateVariable): |
||||
return None |
||||
if not isinstance(exp.expression.value.type, ArrayType): |
||||
return None |
||||
return exp.expression.value |
||||
|
||||
@staticmethod |
||||
def _is_loop_referencing_array_length( |
||||
node: Node, visited: Set[Node], array: StateVariable, depth: int |
||||
) -> True: |
||||
""" |
||||
For a given loop, checks if it references `array.length` at some point. |
||||
Will also return True if `array.length` is referenced but not changed. |
||||
This may potentially generate false negatives in the detector, but it was done this way because: |
||||
- situations when array `length` is referenced but not modified in loop are rare |
||||
- checking if `array.length` is indeed modified would require much more work |
||||
""" |
||||
visited.add(node) |
||||
if node.type == NodeType.STARTLOOP: |
||||
depth += 1 |
||||
if node.type == NodeType.ENDLOOP: |
||||
depth -= 1 |
||||
if depth == 0: |
||||
return False |
||||
|
||||
# Array length may change in the following situations: |
||||
# - when `push` is called |
||||
# - when `pop` is called |
||||
# - when `delete` is called on the entire array |
||||
# - when external function call is made (instructions from internal function calls are already in |
||||
# `node.all_slithir_operations()`, so we don't need to handle internal calls separately) |
||||
if node.type == NodeType.EXPRESSION: |
||||
for op in node.all_slithir_operations(): |
||||
if isinstance(op, Length) and op.value == array: |
||||
# op accesses array.length, not necessarily modifying it |
||||
return True |
||||
if isinstance(op, Delete): |
||||
# take into account only delete entire array, since delete array[i] doesn't change `array.length` |
||||
if ( |
||||
isinstance(op.expression, UnaryOperation) |
||||
and isinstance(op.expression.expression, Identifier) |
||||
and op.expression.expression.value == array |
||||
): |
||||
return True |
||||
if ( |
||||
isinstance(op, HighLevelCall) |
||||
and isinstance(op.function, Function) |
||||
and not op.function.view |
||||
and not op.function.pure |
||||
): |
||||
return True |
||||
|
||||
for son in node.sons: |
||||
if son not in visited: |
||||
if CacheArrayLength._is_loop_referencing_array_length(son, visited, array, depth): |
||||
return True |
||||
return False |
||||
|
||||
@staticmethod |
||||
def _handle_loops(nodes: List[Node], non_optimal_array_len_usages: List[Node]) -> None: |
||||
""" |
||||
For each loop, checks if it has a comparison with `length` array member and, if it has, checks whether that |
||||
array size could potentially change in that loop. |
||||
If it cannot, the loop condition is added to `non_optimal_array_len_usages`. |
||||
There may be some false negatives here - see docs for `_is_loop_referencing_array_length` for more information. |
||||
""" |
||||
for node in nodes: |
||||
if node.type == NodeType.STARTLOOP: |
||||
if_node = node.sons[0] |
||||
if if_node.type != NodeType.IFLOOP: |
||||
continue |
||||
if not isinstance(if_node.expression, BinaryOperation): |
||||
continue |
||||
exp: BinaryOperation = if_node.expression |
||||
if not CacheArrayLength._is_identifier_member_access_comparison(exp): |
||||
continue |
||||
array: StateVariable |
||||
if isinstance(exp.expression_right, MemberAccess): |
||||
array = CacheArrayLength._extract_array_from_length_member_access( |
||||
exp.expression_right |
||||
) |
||||
else: # isinstance(exp.expression_left, MemberAccess) == True |
||||
array = CacheArrayLength._extract_array_from_length_member_access( |
||||
exp.expression_left |
||||
) |
||||
if array is None: |
||||
continue |
||||
|
||||
visited: Set[Node] = set() |
||||
if not CacheArrayLength._is_loop_referencing_array_length( |
||||
if_node, visited, array, 1 |
||||
): |
||||
non_optimal_array_len_usages.append(if_node) |
||||
|
||||
@staticmethod |
||||
def _get_non_optimal_array_len_usages_for_function(f: Function) -> List[Node]: |
||||
""" |
||||
Finds non-optimal usages of array length in loop conditions in a given function. |
||||
""" |
||||
non_optimal_array_len_usages: List[Node] = [] |
||||
CacheArrayLength._handle_loops(f.nodes, non_optimal_array_len_usages) |
||||
|
||||
return non_optimal_array_len_usages |
||||
|
||||
@staticmethod |
||||
def _get_non_optimal_array_len_usages(functions: List[Function]) -> List[Node]: |
||||
""" |
||||
Finds non-optimal usages of array length in loop conditions in given functions. |
||||
""" |
||||
non_optimal_array_len_usages: List[Node] = [] |
||||
|
||||
for f in functions: |
||||
non_optimal_array_len_usages += ( |
||||
CacheArrayLength._get_non_optimal_array_len_usages_for_function(f) |
||||
) |
||||
|
||||
return non_optimal_array_len_usages |
||||
|
||||
def _detect(self): |
||||
results = [] |
||||
|
||||
non_optimal_array_len_usages = CacheArrayLength._get_non_optimal_array_len_usages( |
||||
self.compilation_unit.functions |
||||
) |
||||
for usage in non_optimal_array_len_usages: |
||||
info = [ |
||||
"Loop condition ", |
||||
f"`{usage.source_mapping.content}` ", |
||||
f"({usage.source_mapping}) ", |
||||
"should use cached array length instead of referencing `length` member " |
||||
"of the storage array.\n ", |
||||
] |
||||
res = self.generate_result(info) |
||||
results.append(res) |
||||
return results |
@ -0,0 +1,104 @@ |
||||
""" |
||||
Module detecting usage of more than one dynamic type in abi.encodePacked() arguments which could lead to collision |
||||
""" |
||||
|
||||
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification |
||||
from slither.core.declarations.solidity_variables import SolidityFunction |
||||
from slither.slithir.operations import SolidityCall |
||||
from slither.analyses.data_dependency.data_dependency import is_tainted |
||||
from slither.core.solidity_types import ElementaryType |
||||
from slither.core.solidity_types import ArrayType |
||||
|
||||
|
||||
def _is_dynamic_type(arg): |
||||
""" |
||||
Args: |
||||
arg (function argument) |
||||
Returns: |
||||
Bool |
||||
""" |
||||
if isinstance(arg.type, ElementaryType) and (arg.type.name in ["string", "bytes"]): |
||||
return True |
||||
if isinstance(arg.type, ArrayType) and arg.type.length is None: |
||||
return True |
||||
|
||||
return False |
||||
|
||||
|
||||
def _detect_abi_encodePacked_collision(contract): |
||||
""" |
||||
Args: |
||||
contract (Contract) |
||||
Returns: |
||||
list((Function), (list (Node))) |
||||
""" |
||||
ret = [] |
||||
# pylint: disable=too-many-nested-blocks |
||||
for f in contract.functions_and_modifiers_declared: |
||||
for n in f.nodes: |
||||
for ir in n.irs: |
||||
if isinstance(ir, SolidityCall) and ir.function == SolidityFunction( |
||||
"abi.encodePacked()" |
||||
): |
||||
dynamic_type_count = 0 |
||||
for arg in ir.arguments: |
||||
if is_tainted(arg, contract) and _is_dynamic_type(arg): |
||||
dynamic_type_count += 1 |
||||
elif dynamic_type_count > 1: |
||||
ret.append((f, n)) |
||||
dynamic_type_count = 0 |
||||
else: |
||||
dynamic_type_count = 0 |
||||
if dynamic_type_count > 1: |
||||
ret.append((f, n)) |
||||
return ret |
||||
|
||||
|
||||
class EncodePackedCollision(AbstractDetector): |
||||
""" |
||||
Detect usage of more than one dynamic type in abi.encodePacked() arguments which could to collision |
||||
""" |
||||
|
||||
ARGUMENT = "encode-packed-collision" |
||||
HELP = "ABI encodePacked Collision" |
||||
IMPACT = DetectorClassification.HIGH |
||||
CONFIDENCE = DetectorClassification.HIGH |
||||
|
||||
WIKI = ( |
||||
"https://github.com/crytic/slither/wiki/Detector-Documentation#abi-encodePacked-collision" |
||||
) |
||||
|
||||
WIKI_TITLE = "ABI encodePacked Collision" |
||||
WIKI_DESCRIPTION = """Detect collision due to dynamic type usages in `abi.encodePacked`""" |
||||
|
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
contract Sign { |
||||
function get_hash_for_signature(string name, string doc) external returns(bytes32) { |
||||
return keccak256(abi.encodePacked(name, doc)); |
||||
} |
||||
} |
||||
``` |
||||
Bob calls `get_hash_for_signature` with (`bob`, `This is the content`). The hash returned is used as an ID. |
||||
Eve creates a collision with the ID using (`bo`, `bThis is the content`) and compromises the system. |
||||
""" |
||||
WIKI_RECOMMENDATION = """Do not use more than one dynamic type in `abi.encodePacked()` |
||||
(see the [Solidity documentation](https://solidity.readthedocs.io/en/v0.5.10/abi-spec.html?highlight=abi.encodePacked#non-standard-packed-modeDynamic)). |
||||
Use `abi.encode()`, preferably.""" |
||||
|
||||
def _detect(self): |
||||
"""Detect usage of more than one dynamic type in abi.encodePacked(..) arguments which could lead to collision""" |
||||
results = [] |
||||
for c in self.compilation_unit.contracts: |
||||
values = _detect_abi_encodePacked_collision(c) |
||||
for func, node in values: |
||||
info = [ |
||||
func, |
||||
" calls abi.encodePacked() with multiple dynamic arguments:\n\t- ", |
||||
node, |
||||
"\n", |
||||
] |
||||
json = self.generate_result(info) |
||||
results.append(json) |
||||
|
||||
return results |
@ -0,0 +1,223 @@ |
||||
from typing import List |
||||
|
||||
from slither.core.declarations import Contract, Structure, Enum |
||||
from slither.core.declarations.using_for_top_level import UsingForTopLevel |
||||
from slither.core.solidity_types import ( |
||||
UserDefinedType, |
||||
Type, |
||||
ElementaryType, |
||||
TypeAlias, |
||||
MappingType, |
||||
ArrayType, |
||||
) |
||||
from slither.core.solidity_types.elementary_type import Uint, Int, Byte |
||||
from slither.detectors.abstract_detector import ( |
||||
AbstractDetector, |
||||
DetectorClassification, |
||||
DETECTOR_INFO, |
||||
) |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
def _is_correctly_used(type_: Type, library: Contract) -> bool: |
||||
""" |
||||
Checks if a `using library for type_` statement is used correctly (that is, does library contain any function |
||||
with type_ as the first argument). |
||||
""" |
||||
for f in library.functions: |
||||
if len(f.parameters) == 0: |
||||
continue |
||||
if f.parameters[0].type and not _implicitly_convertible_to(type_, f.parameters[0].type): |
||||
continue |
||||
return True |
||||
return False |
||||
|
||||
|
||||
def _implicitly_convertible_to(type1: Type, type2: Type) -> bool: |
||||
""" |
||||
Returns True if type1 may be implicitly converted to type2. |
||||
""" |
||||
if isinstance(type1, TypeAlias) or isinstance(type2, TypeAlias): |
||||
if isinstance(type1, TypeAlias) and isinstance(type2, TypeAlias): |
||||
return type1.type == type2.type |
||||
return False |
||||
|
||||
if isinstance(type1, UserDefinedType) and isinstance(type2, UserDefinedType): |
||||
if isinstance(type1.type, Contract) and isinstance(type2.type, Contract): |
||||
return _implicitly_convertible_to_for_contracts(type1.type, type2.type) |
||||
|
||||
if isinstance(type1.type, Structure) and isinstance(type2.type, Structure): |
||||
return type1.type.canonical_name == type2.type.canonical_name |
||||
|
||||
if isinstance(type1.type, Enum) and isinstance(type2.type, Enum): |
||||
return type1.type.canonical_name == type2.type.canonical_name |
||||
|
||||
if isinstance(type1, ElementaryType) and isinstance(type2, ElementaryType): |
||||
return _implicitly_convertible_to_for_elementary_types(type1, type2) |
||||
|
||||
if isinstance(type1, MappingType) and isinstance(type2, MappingType): |
||||
return _implicitly_convertible_to_for_mappings(type1, type2) |
||||
|
||||
if isinstance(type1, ArrayType) and isinstance(type2, ArrayType): |
||||
return _implicitly_convertible_to_for_arrays(type1, type2) |
||||
|
||||
return False |
||||
|
||||
|
||||
def _implicitly_convertible_to_for_arrays(type1: ArrayType, type2: ArrayType) -> bool: |
||||
""" |
||||
Returns True if type1 may be implicitly converted to type2. |
||||
""" |
||||
return _implicitly_convertible_to(type1.type, type2.type) |
||||
|
||||
|
||||
def _implicitly_convertible_to_for_mappings(type1: MappingType, type2: MappingType) -> bool: |
||||
""" |
||||
Returns True if type1 may be implicitly converted to type2. |
||||
""" |
||||
return type1.type_from == type2.type_from and type1.type_to == type2.type_to |
||||
|
||||
|
||||
def _implicitly_convertible_to_for_elementary_types( |
||||
type1: ElementaryType, type2: ElementaryType |
||||
) -> bool: |
||||
""" |
||||
Returns True if type1 may be implicitly converted to type2. |
||||
""" |
||||
if type1.type == "bool" and type2.type == "bool": |
||||
return True |
||||
if type1.type == "string" and type2.type == "string": |
||||
return True |
||||
if type1.type == "bytes" and type2.type == "bytes": |
||||
return True |
||||
if type1.type == "address" and type2.type == "address": |
||||
return _implicitly_convertible_to_for_addresses(type1, type2) |
||||
if type1.type in Uint and type2.type in Uint: |
||||
return _implicitly_convertible_to_for_uints(type1, type2) |
||||
if type1.type in Int and type2.type in Int: |
||||
return _implicitly_convertible_to_for_ints(type1, type2) |
||||
if ( |
||||
type1.type != "bytes" |
||||
and type2.type != "bytes" |
||||
and type1.type in Byte |
||||
and type2.type in Byte |
||||
): |
||||
return _implicitly_convertible_to_for_bytes(type1, type2) |
||||
return False |
||||
|
||||
|
||||
def _implicitly_convertible_to_for_bytes(type1: ElementaryType, type2: ElementaryType) -> bool: |
||||
""" |
||||
Returns True if type1 may be implicitly converted to type2 assuming they are both bytes. |
||||
""" |
||||
assert type1.type in Byte and type2.type in Byte |
||||
assert type1.size is not None |
||||
assert type2.size is not None |
||||
|
||||
return type1.size <= type2.size |
||||
|
||||
|
||||
def _implicitly_convertible_to_for_addresses(type1: ElementaryType, type2: ElementaryType) -> bool: |
||||
""" |
||||
Returns True if type1 may be implicitly converted to type2 assuming they are both addresses. |
||||
""" |
||||
assert type1.type == "address" and type2.type == "address" |
||||
# payable attribute to be implemented; for now, always return True |
||||
return True |
||||
|
||||
|
||||
def _implicitly_convertible_to_for_ints(type1: ElementaryType, type2: ElementaryType) -> bool: |
||||
""" |
||||
Returns True if type1 may be implicitly converted to type2 assuming they are both ints. |
||||
""" |
||||
assert type1.type in Int and type2.type in Int |
||||
assert type1.size is not None |
||||
assert type2.size is not None |
||||
|
||||
return type1.size <= type2.size |
||||
|
||||
|
||||
def _implicitly_convertible_to_for_uints(type1: ElementaryType, type2: ElementaryType) -> bool: |
||||
""" |
||||
Returns True if type1 may be implicitly converted to type2 assuming they are both uints. |
||||
""" |
||||
assert type1.type in Uint and type2.type in Uint |
||||
assert type1.size is not None |
||||
assert type2.size is not None |
||||
|
||||
return type1.size <= type2.size |
||||
|
||||
|
||||
def _implicitly_convertible_to_for_contracts(contract1: Contract, contract2: Contract) -> bool: |
||||
""" |
||||
Returns True if contract1 may be implicitly converted to contract2. |
||||
""" |
||||
return contract1 == contract2 or contract2 in contract1.inheritance |
||||
|
||||
|
||||
class IncorrectUsingFor(AbstractDetector): |
||||
""" |
||||
Detector for incorrect using-for statement usage. |
||||
""" |
||||
|
||||
ARGUMENT = "incorrect-using-for" |
||||
HELP = "Detects using-for statement usage when no function from a given library matches a given type" |
||||
IMPACT = DetectorClassification.INFORMATIONAL |
||||
CONFIDENCE = DetectorClassification.HIGH |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-using-for-usage" |
||||
|
||||
WIKI_TITLE = "Incorrect usage of using-for statement" |
||||
WIKI_DESCRIPTION = ( |
||||
"In Solidity, it is possible to use libraries for certain types, by the `using-for` statement " |
||||
"(`using <library> for <type>`). However, the Solidity compiler doesn't check whether a given " |
||||
"library has at least one function matching a given type. If it doesn't, such a statement has " |
||||
"no effect and may be confusing. " |
||||
) |
||||
|
||||
# region wiki_exploit_scenario |
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
library L { |
||||
function f(bool) public pure {} |
||||
} |
||||
|
||||
using L for uint; |
||||
``` |
||||
Such a code will compile despite the fact that `L` has no function with `uint` as its first argument.""" |
||||
# endregion wiki_exploit_scenario |
||||
WIKI_RECOMMENDATION = ( |
||||
"Make sure that the libraries used in `using-for` statements have at least one function " |
||||
"matching a type used in these statements. " |
||||
) |
||||
|
||||
def _append_result( |
||||
self, results: List[Output], uf: UsingForTopLevel, type_: Type, library: Contract |
||||
) -> None: |
||||
info: DETECTOR_INFO = [ |
||||
f"using-for statement at {uf.source_mapping} is incorrect - no matching function for {type_} found in ", |
||||
library, |
||||
".\n", |
||||
] |
||||
res = self.generate_result(info) |
||||
results.append(res) |
||||
|
||||
def _detect(self) -> List[Output]: |
||||
results: List[Output] = [] |
||||
|
||||
for uf in self.compilation_unit.using_for_top_level: |
||||
# UsingForTopLevel.using_for is a dict with a single entry, which is mapped to a list of functions/libraries |
||||
# the following code extracts the type from using-for and skips using-for statements with functions |
||||
type_ = list(uf.using_for.keys())[0] |
||||
for lib_or_fcn in uf.using_for[type_]: |
||||
# checking for using-for with functions is already performed by the compiler; we only consider libraries |
||||
if isinstance(lib_or_fcn, UserDefinedType): |
||||
lib_or_fcn_type = lib_or_fcn.type |
||||
if ( |
||||
isinstance(type_, Type) |
||||
and isinstance(lib_or_fcn_type, Contract) |
||||
and not _is_correctly_used(type_, lib_or_fcn_type) |
||||
): |
||||
self._append_result(results, uf, type_, lib_or_fcn_type) |
||||
|
||||
return results |
@ -0,0 +1,35 @@ |
||||
""" |
||||
Lines of Code (LOC) printer |
||||
|
||||
Definitions: |
||||
cloc: comment lines of code containing only comments |
||||
sloc: source lines of code with no whitespace or comments |
||||
loc: all lines of code including whitespace and comments |
||||
src: source files (excluding tests and dependencies) |
||||
dep: dependency files |
||||
test: test files |
||||
""" |
||||
|
||||
from slither.printers.abstract_printer import AbstractPrinter |
||||
from slither.utils.loc import compute_loc_metrics |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
class LocPrinter(AbstractPrinter): |
||||
ARGUMENT = "loc" |
||||
HELP = """Count the total number lines of code (LOC), source lines of code (SLOC), \ |
||||
and comment lines of code (CLOC) found in source files (SRC), dependencies (DEP), \ |
||||
and test files (TEST).""" |
||||
|
||||
WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#loc" |
||||
|
||||
def output(self, _filename: str) -> Output: |
||||
# compute loc metrics |
||||
loc = compute_loc_metrics(self.slither) |
||||
|
||||
table = loc.to_pretty_table() |
||||
txt = "Lines of Code \n" + str(table) |
||||
self.info(txt) |
||||
res = self.generate_output(txt) |
||||
res.add_pretty_table(table, "Code Lines") |
||||
return res |
@ -0,0 +1,21 @@ |
||||
# Slither-interface |
||||
|
||||
Generates code for a Solidity interface from contract |
||||
|
||||
## Usage |
||||
|
||||
Run `slither-interface <ContractName> <source file or deployment address>`. |
||||
|
||||
## CLI Interface |
||||
```shell |
||||
positional arguments: |
||||
contract_source The name of the contract (case sensitive) followed by the deployed contract address if verified on etherscan or project directory/filename for local contracts. |
||||
|
||||
optional arguments: |
||||
-h, --help show this help message and exit |
||||
--unroll-structs Whether to use structures' underlying types instead of the user-defined type |
||||
--exclude-events Excludes event signatures in the interface |
||||
--exclude-errors Excludes custom error signatures in the interface |
||||
--exclude-enums Excludes enum definitions in the interface |
||||
--exclude-structs Exclude struct definitions in the interface |
||||
``` |
@ -0,0 +1,106 @@ |
||||
import argparse |
||||
import logging |
||||
from pathlib import Path |
||||
|
||||
from crytic_compile import cryticparser |
||||
|
||||
from slither import Slither |
||||
from slither.utils.code_generation import generate_interface |
||||
|
||||
logging.basicConfig() |
||||
logger = logging.getLogger("Slither-Interface") |
||||
logger.setLevel(logging.INFO) |
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace: |
||||
""" |
||||
Parse the underlying arguments for the program. |
||||
:return: Returns the arguments for the program. |
||||
""" |
||||
parser = argparse.ArgumentParser( |
||||
description="Generates code for a Solidity interface from contract", |
||||
usage=("slither-interface <ContractName> <source file or deployment address>"), |
||||
) |
||||
|
||||
parser.add_argument( |
||||
"contract_source", |
||||
help="The name of the contract (case sensitive) followed by the deployed contract address if verified on etherscan or project directory/filename for local contracts.", |
||||
nargs="+", |
||||
) |
||||
|
||||
parser.add_argument( |
||||
"--unroll-structs", |
||||
help="Whether to use structures' underlying types instead of the user-defined type", |
||||
default=False, |
||||
action="store_true", |
||||
) |
||||
|
||||
parser.add_argument( |
||||
"--exclude-events", |
||||
help="Excludes event signatures in the interface", |
||||
default=False, |
||||
action="store_true", |
||||
) |
||||
|
||||
parser.add_argument( |
||||
"--exclude-errors", |
||||
help="Excludes custom error signatures in the interface", |
||||
default=False, |
||||
action="store_true", |
||||
) |
||||
|
||||
parser.add_argument( |
||||
"--exclude-enums", |
||||
help="Excludes enum definitions in the interface", |
||||
default=False, |
||||
action="store_true", |
||||
) |
||||
|
||||
parser.add_argument( |
||||
"--exclude-structs", |
||||
help="Exclude struct definitions in the interface", |
||||
default=False, |
||||
action="store_true", |
||||
) |
||||
|
||||
cryticparser.init(parser) |
||||
|
||||
return parser.parse_args() |
||||
|
||||
|
||||
def main() -> None: |
||||
args = parse_args() |
||||
|
||||
contract_name, target = args.contract_source |
||||
slither = Slither(target, **vars(args)) |
||||
|
||||
_contract = slither.get_contract_from_name(contract_name)[0] |
||||
|
||||
interface = generate_interface( |
||||
contract=_contract, |
||||
unroll_structs=args.unroll_structs, |
||||
include_events=not args.exclude_events, |
||||
include_errors=not args.exclude_errors, |
||||
include_enums=not args.exclude_enums, |
||||
include_structs=not args.exclude_structs, |
||||
) |
||||
|
||||
# add version pragma |
||||
if _contract.compilation_unit.pragma_directives: |
||||
interface = ( |
||||
f"pragma solidity {_contract.compilation_unit.pragma_directives[0].version};\n\n" |
||||
+ interface |
||||
) |
||||
|
||||
# write interface to file |
||||
export = Path("crytic-export", "interfaces") |
||||
export.mkdir(parents=True, exist_ok=True) |
||||
filename = f"I{contract_name}.sol" |
||||
path = Path(export, filename) |
||||
logger.info(f" Interface exported to {path}") |
||||
with open(path, "w", encoding="utf8") as f: |
||||
f.write(interface) |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
main() |
@ -1 +1 @@ |
||||
from .read_storage import SlitherReadStorage |
||||
from .read_storage import SlitherReadStorage, RpcInfo |
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue