mirror of https://github.com/crytic/slither
commit
80efe77dfc
@ -0,0 +1,8 @@ |
||||
--- |
||||
version: 2 |
||||
updates: |
||||
- package-ecosystem: "github-actions" |
||||
directory: "/" |
||||
target-branch: "dev" |
||||
schedule: |
||||
interval: "weekly" |
@ -0,0 +1,32 @@ |
||||
{ |
||||
"problemMatcher": [ |
||||
{ |
||||
"owner": "pylint-error", |
||||
"severity": "error", |
||||
"pattern": [ |
||||
{ |
||||
"regexp": "^(.+):(\\d+):(\\d+):\\s(([EF]\\d{4}):\\s.+)$", |
||||
"file": 1, |
||||
"line": 2, |
||||
"column": 3, |
||||
"message": 4, |
||||
"code": 5 |
||||
} |
||||
] |
||||
}, |
||||
{ |
||||
"owner": "pylint-warning", |
||||
"severity": "warning", |
||||
"pattern": [ |
||||
{ |
||||
"regexp": "^(.+):(\\d+):(\\d+):\\s(([CRW]\\d{4}):\\s.+)$", |
||||
"file": 1, |
||||
"line": 2, |
||||
"column": 3, |
||||
"message": 4, |
||||
"code": 5 |
||||
} |
||||
] |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,22 @@ |
||||
{ |
||||
"problemMatcher": [ |
||||
{ |
||||
"owner": "yamllint", |
||||
"pattern": [ |
||||
{ |
||||
"regexp": "^(.*\\.ya?ml)$", |
||||
"file": 1 |
||||
}, |
||||
{ |
||||
"regexp": "^\\s{2}(\\d+):(\\d+)\\s+(error|warning)\\s+(.*?)\\s+\\((.*)\\)$", |
||||
"line": 1, |
||||
"column": 2, |
||||
"severity": 3, |
||||
"message": 4, |
||||
"code": 5, |
||||
"loop": true |
||||
} |
||||
] |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,53 @@ |
||||
name: Publish to PyPI |
||||
|
||||
on: |
||||
release: |
||||
types: [published] |
||||
|
||||
jobs: |
||||
build-release: |
||||
|
||||
runs-on: ubuntu-latest |
||||
|
||||
steps: |
||||
- uses: actions/checkout@v4 |
||||
|
||||
- name: Set up Python |
||||
uses: actions/setup-python@v5 |
||||
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@v4 |
||||
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@v4 |
||||
with: |
||||
name: slither-dists |
||||
path: dist/ |
||||
|
||||
- name: publish |
||||
uses: pypa/gh-action-pypi-publish@v1.8.11 |
||||
|
||||
- name: sign |
||||
uses: sigstore/gh-action-sigstore-python@v2.1.1 |
||||
with: |
||||
inputs: ./dist/*.tar.gz ./dist/*.whl |
||||
release-signing-artifacts: true |
@ -0,0 +1,64 @@ |
||||
cff-version: 1.2.0 |
||||
title: Slither Analyzer |
||||
message: >- |
||||
If you use this software, please cite it using the |
||||
metadata from this file. |
||||
type: software |
||||
authors: |
||||
- given-names: Josselin |
||||
family-names: Feist |
||||
- given-names: Gustavo |
||||
family-names: Grieco |
||||
- given-names: Alex |
||||
family-names: Groce |
||||
identifiers: |
||||
- type: doi |
||||
value: 10.48550/arXiv.1908.09878 |
||||
description: arXiv.1908.09878 |
||||
- type: url |
||||
value: 'https://arxiv.org/abs/1908.09878' |
||||
description: arxiv |
||||
- type: doi |
||||
value: 10.1109/wetseb.2019.00008 |
||||
repository-code: 'https://github.com/crytic/slither' |
||||
url: 'https://www.trailofbits.com/' |
||||
repository-artifact: 'https://github.com/crytic/slither/releases' |
||||
abstract: >- |
||||
Slither is a static analysis framework designed to provide |
||||
rich information about Ethereum smart contracts. |
||||
|
||||
It works by converting Solidity smart contracts into an |
||||
intermediate representation called SlithIR. |
||||
|
||||
SlithIR uses Static Single Assignment (SSA) form and a |
||||
reduced instruction set to ease implementation of analyses |
||||
while preserving semantic information that would be lost |
||||
in transforming Solidity to bytecode. |
||||
|
||||
Slither allows for the application of commonly used |
||||
program analysis techniques like dataflow and taint |
||||
tracking. |
||||
|
||||
|
||||
Our framework has four main use cases: |
||||
|
||||
(1) automated detection of vulnerabilities, |
||||
|
||||
(2) automated detection of code optimization |
||||
opportunities, |
||||
|
||||
(3) improvement of the user's understanding of the |
||||
contracts, and |
||||
|
||||
(4) assistance with code review. |
||||
keywords: |
||||
- Ethereum |
||||
- Static Analysis |
||||
- Smart contracts |
||||
- EVM |
||||
- bug detection |
||||
- Software Engineering |
||||
license: AGPL-3.0-only |
||||
commit: 3d4f934d3228f072b7df2c5e7252c64df4601bc8 |
||||
version: 0.9.5 |
||||
date-released: '2023-06-28' |
@ -0,0 +1,25 @@ |
||||
from typing import TYPE_CHECKING |
||||
|
||||
from slither.core.declarations.contract_level import ContractLevel |
||||
from slither.core.declarations import Event |
||||
|
||||
if TYPE_CHECKING: |
||||
from slither.core.declarations import Contract |
||||
|
||||
|
||||
class EventContract(Event, ContractLevel): |
||||
def is_declared_by(self, contract: "Contract") -> bool: |
||||
""" |
||||
Check if the element is declared by the contract |
||||
:param contract: |
||||
:return: |
||||
""" |
||||
return self.contract == contract |
||||
|
||||
@property |
||||
def canonical_name(self) -> str: |
||||
"""Return the function signature as a str |
||||
Returns: |
||||
str: contract.func_name(type1,type2) |
||||
""" |
||||
return self.contract.name + "." + self.full_name |
@ -0,0 +1,13 @@ |
||||
from typing import TYPE_CHECKING |
||||
|
||||
from slither.core.declarations import Event |
||||
from slither.core.declarations.top_level import TopLevel |
||||
|
||||
if TYPE_CHECKING: |
||||
from slither.core.scope.scope import FileScope |
||||
|
||||
|
||||
class EventTopLevel(Event, TopLevel): |
||||
def __init__(self, scope: "FileScope") -> None: |
||||
super().__init__() |
||||
self.file_scope: "FileScope" = scope |
@ -0,0 +1,6 @@ |
||||
from slither.core.expressions.identifier import Identifier |
||||
|
||||
|
||||
class SelfIdentifier(Identifier): |
||||
def __str__(self): |
||||
return "self." + str(self._value) |
@ -1,2 +1,8 @@ |
||||
from .state_variable import StateVariable |
||||
from .variable import Variable |
||||
from .local_variable_init_from_tuple import LocalVariableInitFromTuple |
||||
from .local_variable import LocalVariable |
||||
from .top_level_variable import TopLevelVariable |
||||
from .event_variable import EventVariable |
||||
from .function_type_variable import FunctionTypeVariable |
||||
from .structure_variable import StructureVariable |
||||
|
@ -0,0 +1,93 @@ |
||||
from typing import List, Optional |
||||
|
||||
from slither.core.declarations import SolidityFunction, Function |
||||
from slither.detectors.abstract_detector import ( |
||||
AbstractDetector, |
||||
DetectorClassification, |
||||
DETECTOR_INFO, |
||||
) |
||||
from slither.slithir.operations import SolidityCall |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
def _assembly_node(function: Function) -> Optional[SolidityCall]: |
||||
""" |
||||
Check if there is a node that use return in assembly |
||||
|
||||
Args: |
||||
function: |
||||
|
||||
Returns: |
||||
|
||||
""" |
||||
|
||||
for ir in function.all_slithir_operations(): |
||||
if isinstance(ir, SolidityCall) and ir.function == SolidityFunction( |
||||
"return(uint256,uint256)" |
||||
): |
||||
return ir |
||||
return None |
||||
|
||||
|
||||
class IncorrectReturn(AbstractDetector): |
||||
""" |
||||
Check for cases where a return(a,b) is used in an assembly function |
||||
""" |
||||
|
||||
ARGUMENT = "incorrect-return" |
||||
HELP = "If a `return` is incorrectly used in assembly mode." |
||||
IMPACT = DetectorClassification.HIGH |
||||
CONFIDENCE = DetectorClassification.MEDIUM |
||||
|
||||
WIKI = ( |
||||
"https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-return-in-assembly" |
||||
) |
||||
|
||||
WIKI_TITLE = "Incorrect return in assembly" |
||||
WIKI_DESCRIPTION = "Detect if `return` in an assembly block halts unexpectedly the execution." |
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
contract C { |
||||
function f() internal returns (uint a, uint b) { |
||||
assembly { |
||||
return (5, 6) |
||||
} |
||||
} |
||||
|
||||
function g() returns (bool){ |
||||
f(); |
||||
return true; |
||||
} |
||||
} |
||||
``` |
||||
The return statement in `f` will cause execution in `g` to halt. |
||||
The function will return 6 bytes starting from offset 5, instead of returning a boolean.""" |
||||
|
||||
WIKI_RECOMMENDATION = "Use the `leave` statement." |
||||
|
||||
# pylint: disable=too-many-nested-blocks |
||||
def _detect(self) -> List[Output]: |
||||
results: List[Output] = [] |
||||
for c in self.contracts: |
||||
for f in c.functions_and_modifiers_declared: |
||||
|
||||
for node in f.nodes: |
||||
if node.sons: |
||||
for function_called in node.internal_calls: |
||||
if isinstance(function_called, Function): |
||||
found = _assembly_node(function_called) |
||||
if found: |
||||
|
||||
info: DETECTOR_INFO = [ |
||||
f, |
||||
" calls ", |
||||
function_called, |
||||
" which halt the execution ", |
||||
found.node, |
||||
"\n", |
||||
] |
||||
json = self.generate_result(info) |
||||
|
||||
results.append(json) |
||||
|
||||
return results |
@ -0,0 +1,68 @@ |
||||
from typing import List |
||||
|
||||
from slither.core.declarations import SolidityFunction, Function |
||||
from slither.detectors.abstract_detector import ( |
||||
AbstractDetector, |
||||
DetectorClassification, |
||||
DETECTOR_INFO, |
||||
) |
||||
from slither.slithir.operations import SolidityCall |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
class ReturnInsteadOfLeave(AbstractDetector): |
||||
""" |
||||
Check for cases where a return(a,b) is used in an assembly function that also returns two variables |
||||
""" |
||||
|
||||
ARGUMENT = "return-leave" |
||||
HELP = "If a `return` is used instead of a `leave`." |
||||
IMPACT = DetectorClassification.HIGH |
||||
CONFIDENCE = DetectorClassification.MEDIUM |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#return-instead-of-leave-in-assembly" |
||||
|
||||
WIKI_TITLE = "Return instead of leave in assembly" |
||||
WIKI_DESCRIPTION = "Detect if a `return` is used where a `leave` should be used." |
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
contract C { |
||||
function f() internal returns (uint a, uint b) { |
||||
assembly { |
||||
return (5, 6) |
||||
} |
||||
} |
||||
|
||||
} |
||||
``` |
||||
The function will halt the execution, instead of returning a two uint.""" |
||||
|
||||
WIKI_RECOMMENDATION = "Use the `leave` statement." |
||||
|
||||
def _check_function(self, f: Function) -> List[Output]: |
||||
results: List[Output] = [] |
||||
|
||||
for node in f.nodes: |
||||
for ir in node.irs: |
||||
if isinstance(ir, SolidityCall) and ir.function == SolidityFunction( |
||||
"return(uint256,uint256)" |
||||
): |
||||
info: DETECTOR_INFO = [f, " contains an incorrect call to return: ", node, "\n"] |
||||
json = self.generate_result(info) |
||||
|
||||
results.append(json) |
||||
return results |
||||
|
||||
def _detect(self) -> List[Output]: |
||||
results: List[Output] = [] |
||||
for c in self.contracts: |
||||
for f in c.functions_declared: |
||||
|
||||
if ( |
||||
len(f.returns) == 2 |
||||
and f.contains_assembly |
||||
and f.visibility not in ["public", "external"] |
||||
): |
||||
results += self._check_function(f) |
||||
|
||||
return results |
@ -0,0 +1,225 @@ |
||||
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 ", |
||||
usage, |
||||
" 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,93 @@ |
||||
""" |
||||
Module detecting incorrect operator usage for exponentiation where bitwise xor '^' is used instead of '**' |
||||
""" |
||||
from typing import Tuple, List, Union |
||||
|
||||
from slither.core.cfg.node import Node |
||||
from slither.core.declarations import Contract, Function |
||||
from slither.detectors.abstract_detector import ( |
||||
AbstractDetector, |
||||
DetectorClassification, |
||||
DETECTOR_INFO, |
||||
) |
||||
from slither.slithir.operations import Binary, BinaryType, Operation |
||||
from slither.slithir.utils.utils import RVALUE |
||||
from slither.slithir.variables.constant import Constant |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
def _is_constant_candidate(var: Union[RVALUE, Function]) -> bool: |
||||
""" |
||||
Check if the variable is a constant. |
||||
Do not consider variable that are expressed with hexadecimal. |
||||
Something like 2^0xf is likely to be a correct bitwise operator |
||||
:param var: |
||||
:return: |
||||
""" |
||||
return isinstance(var, Constant) and not var.original_value.startswith("0x") |
||||
|
||||
|
||||
def _is_bitwise_xor_on_constant(ir: Operation) -> bool: |
||||
return ( |
||||
isinstance(ir, Binary) |
||||
and ir.type == BinaryType.CARET |
||||
and (_is_constant_candidate(ir.variable_left) or _is_constant_candidate(ir.variable_right)) |
||||
) |
||||
|
||||
|
||||
def _detect_incorrect_operator(contract: Contract) -> List[Tuple[Function, Node]]: |
||||
ret: List[Tuple[Function, Node]] = [] |
||||
f: Function |
||||
for f in contract.functions + contract.modifiers: # type:ignore |
||||
# Heuristic: look for binary expressions with ^ operator where at least one of the operands is a constant, and |
||||
# the constant is not in hex, because hex typically is used with bitwise xor and not exponentiation |
||||
nodes = [node for node in f.nodes for ir in node.irs if _is_bitwise_xor_on_constant(ir)] |
||||
for node in nodes: |
||||
ret.append((f, node)) |
||||
return ret |
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods |
||||
class IncorrectOperatorExponentiation(AbstractDetector): |
||||
""" |
||||
Incorrect operator usage of bitwise xor mistaking it for exponentiation |
||||
""" |
||||
|
||||
ARGUMENT = "incorrect-exp" |
||||
HELP = "Incorrect exponentiation" |
||||
IMPACT = DetectorClassification.HIGH |
||||
CONFIDENCE = DetectorClassification.MEDIUM |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-exponentiation" |
||||
|
||||
WIKI_TITLE = "Incorrect exponentiation" |
||||
WIKI_DESCRIPTION = "Detect use of bitwise `xor ^` instead of exponential `**`" |
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
contract Bug{ |
||||
uint UINT_MAX = 2^256 - 1; |
||||
... |
||||
} |
||||
``` |
||||
Alice deploys a contract in which `UINT_MAX` incorrectly uses `^` operator instead of `**` for exponentiation""" |
||||
|
||||
WIKI_RECOMMENDATION = "Use the correct operator `**` for exponentiation." |
||||
|
||||
def _detect(self) -> List[Output]: |
||||
"""Detect the incorrect operator usage for exponentiation where bitwise xor ^ is used instead of ** |
||||
|
||||
Returns: |
||||
list: (function, node) |
||||
""" |
||||
results: List[Output] = [] |
||||
for c in self.compilation_unit.contracts_derived: |
||||
res = _detect_incorrect_operator(c) |
||||
for (func, node) in res: |
||||
info: DETECTOR_INFO = [ |
||||
func, |
||||
" has bitwise-xor operator ^ instead of the exponentiation operator **: \n", |
||||
] |
||||
info += ["\t - ", node, "\n"] |
||||
results.append(self.generate_result(info)) |
||||
|
||||
return results |
@ -0,0 +1,123 @@ |
||||
from typing import List |
||||
|
||||
from slither.core.cfg.node import Node |
||||
from slither.core.declarations import Contract |
||||
from slither.core.declarations.function import Function |
||||
from slither.core.solidity_types import Type |
||||
from slither.detectors.abstract_detector import ( |
||||
AbstractDetector, |
||||
DetectorClassification, |
||||
DETECTOR_INFO, |
||||
) |
||||
from slither.slithir.operations import LowLevelCall, HighLevelCall |
||||
from slither.analyses.data_dependency.data_dependency import is_tainted |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
class ReturnBomb(AbstractDetector): |
||||
|
||||
ARGUMENT = "return-bomb" |
||||
HELP = "A low level callee may consume all callers gas unexpectedly." |
||||
IMPACT = DetectorClassification.LOW |
||||
CONFIDENCE = DetectorClassification.MEDIUM |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#return-bomb" |
||||
|
||||
WIKI_TITLE = "Return Bomb" |
||||
WIKI_DESCRIPTION = "A low level callee may consume all callers gas unexpectedly." |
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
//Modified from https://github.com/nomad-xyz/ExcessivelySafeCall |
||||
contract BadGuy { |
||||
function youveActivateMyTrapCard() external pure returns (bytes memory) { |
||||
assembly{ |
||||
revert(0, 1000000) |
||||
} |
||||
} |
||||
} |
||||
|
||||
contract Mark { |
||||
function oops(address badGuy) public{ |
||||
bool success; |
||||
bytes memory ret; |
||||
|
||||
// Mark pays a lot of gas for this copy |
||||
//(success, ret) = badGuy.call{gas:10000}( |
||||
(success, ret) = badGuy.call( |
||||
abi.encodeWithSelector( |
||||
BadGuy.youveActivateMyTrapCard.selector |
||||
) |
||||
); |
||||
|
||||
// Mark may OOG here, preventing local state changes |
||||
//importantCleanup(); |
||||
} |
||||
} |
||||
|
||||
``` |
||||
After Mark calls BadGuy bytes are copied from returndata to memory, the memory expansion cost is paid. This means that when using a standard solidity call, the callee can "returnbomb" the caller, imposing an arbitrary gas cost. |
||||
Callee unexpectedly makes the caller OOG. |
||||
""" |
||||
|
||||
WIKI_RECOMMENDATION = "Avoid unlimited implicit decoding of returndata." |
||||
|
||||
@staticmethod |
||||
def is_dynamic_type(ty: Type) -> bool: |
||||
# ty.is_dynamic ? |
||||
name = str(ty) |
||||
if "[]" in name or name in ("bytes", "string"): |
||||
return True |
||||
return False |
||||
|
||||
def get_nodes_for_function(self, function: Function, contract: Contract) -> List[Node]: |
||||
nodes = [] |
||||
for node in function.nodes: |
||||
for ir in node.irs: |
||||
if isinstance(ir, (HighLevelCall, LowLevelCall)): |
||||
if not is_tainted(ir.destination, contract): # type:ignore |
||||
# Only interested if the target address is controlled/tainted |
||||
continue |
||||
|
||||
if isinstance(ir, HighLevelCall) and isinstance(ir.function, Function): |
||||
# in normal highlevel calls return bombs are _possible_ |
||||
# if the return type is dynamic and the caller tries to copy and decode large data |
||||
has_dyn = False |
||||
if ir.function.return_type: |
||||
has_dyn = any( |
||||
self.is_dynamic_type(ty) for ty in ir.function.return_type |
||||
) |
||||
|
||||
if not has_dyn: |
||||
continue |
||||
|
||||
# If a gas budget was specified then the |
||||
# user may not know about the return bomb |
||||
if ir.call_gas is None: |
||||
# if a gas budget was NOT specified then the caller |
||||
# may already suspect the call may spend all gas? |
||||
continue |
||||
|
||||
nodes.append(node) |
||||
# TODO: check that there is some state change after the call |
||||
|
||||
return nodes |
||||
|
||||
def _detect(self) -> List[Output]: |
||||
results = [] |
||||
|
||||
for contract in self.compilation_unit.contracts: |
||||
for function in contract.functions_declared: |
||||
nodes = self.get_nodes_for_function(function, contract) |
||||
if nodes: |
||||
info: DETECTOR_INFO = [ |
||||
function, |
||||
" tries to limit the gas of an external call that controls implicit decoding\n", |
||||
] |
||||
|
||||
for node in sorted(nodes, key=lambda x: x.node_id): |
||||
info += ["\t", node, "\n"] |
||||
|
||||
res = self.generate_result(info) |
||||
results.append(res) |
||||
|
||||
return results |
@ -0,0 +1,69 @@ |
||||
from typing import List |
||||
from slither.detectors.abstract_detector import ( |
||||
AbstractDetector, |
||||
DetectorClassification, |
||||
DETECTOR_INFO, |
||||
) |
||||
from slither.slithir.operations import ( |
||||
Binary, |
||||
BinaryType, |
||||
) |
||||
|
||||
from slither.core.declarations import Function |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
class TautologicalCompare(AbstractDetector): |
||||
""" |
||||
Same variable comparison detector |
||||
""" |
||||
|
||||
ARGUMENT = "tautological-compare" |
||||
HELP = "Comparing a variable to itself always returns true or false, depending on comparison" |
||||
IMPACT = DetectorClassification.MEDIUM |
||||
CONFIDENCE = DetectorClassification.HIGH |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#tautological-compare" |
||||
|
||||
WIKI_TITLE = "Tautological compare" |
||||
WIKI_DESCRIPTION = "A variable compared to itself is probably an error as it will always return `true` for `==`, `>=`, `<=` and always `false` for `<`, `>` and `!=`." |
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
function check(uint a) external returns(bool){ |
||||
return (a >= a); |
||||
} |
||||
``` |
||||
`check` always return true.""" |
||||
|
||||
WIKI_RECOMMENDATION = "Remove comparison or compare to different value." |
||||
|
||||
def _check_function(self, f: Function) -> List[Output]: |
||||
affected_nodes = set() |
||||
for node in f.nodes: |
||||
for ir in node.irs: |
||||
if isinstance(ir, Binary): |
||||
if ir.type in [ |
||||
BinaryType.GREATER, |
||||
BinaryType.GREATER_EQUAL, |
||||
BinaryType.LESS, |
||||
BinaryType.LESS_EQUAL, |
||||
BinaryType.EQUAL, |
||||
BinaryType.NOT_EQUAL, |
||||
]: |
||||
if ir.variable_left == ir.variable_right: |
||||
affected_nodes.add(node) |
||||
|
||||
results = [] |
||||
for n in affected_nodes: |
||||
info: DETECTOR_INFO = [f, " compares a variable to itself:\n\t", n, "\n"] |
||||
res = self.generate_result(info) |
||||
results.append(res) |
||||
return results |
||||
|
||||
def _detect(self): |
||||
results = [] |
||||
|
||||
for f in self.compilation_unit.functions_and_modifiers: |
||||
results.extend(self._check_function(f)) |
||||
|
||||
return results |
@ -0,0 +1,58 @@ |
||||
""" |
||||
CK Metrics are a suite of six software metrics proposed by Chidamber and Kemerer in 1994. |
||||
These metrics are used to measure the complexity of a class. |
||||
https://en.wikipedia.org/wiki/Programming_complexity |
||||
|
||||
- Response For a Class (RFC) is a metric that measures the number of unique method calls within a class. |
||||
- Number of Children (NOC) is a metric that measures the number of children a class has. |
||||
- Depth of Inheritance Tree (DIT) is a metric that measures the number of parent classes a class has. |
||||
- Coupling Between Object Classes (CBO) is a metric that measures the number of classes a class is coupled to. |
||||
|
||||
Not implemented: |
||||
- Lack of Cohesion of Methods (LCOM) is a metric that measures the lack of cohesion in methods. |
||||
- Weighted Methods per Class (WMC) is a metric that measures the complexity of a class. |
||||
|
||||
During the calculation of the metrics above, there are a number of other intermediate metrics that are calculated. |
||||
These are also included in the output: |
||||
- State variables: total number of state variables |
||||
- Constants: total number of constants |
||||
- Immutables: total number of immutables |
||||
- Public: total number of public functions |
||||
- External: total number of external functions |
||||
- Internal: total number of internal functions |
||||
- Private: total number of private functions |
||||
- Mutating: total number of state mutating functions |
||||
- View: total number of view functions |
||||
- Pure: total number of pure functions |
||||
- External mutating: total number of external mutating functions |
||||
- No auth or onlyOwner: total number of functions without auth or onlyOwner modifiers |
||||
- No modifiers: total number of functions without modifiers |
||||
- Ext calls: total number of external calls |
||||
|
||||
""" |
||||
from slither.printers.abstract_printer import AbstractPrinter |
||||
from slither.utils.ck import CKMetrics |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
class CK(AbstractPrinter): |
||||
ARGUMENT = "ck" |
||||
HELP = "Chidamber and Kemerer (CK) complexity metrics and related function attributes" |
||||
|
||||
WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#ck" |
||||
|
||||
def output(self, _filename: str) -> Output: |
||||
if len(self.contracts) == 0: |
||||
return self.generate_output("No contract found") |
||||
|
||||
ck = CKMetrics(self.contracts) |
||||
|
||||
res = self.generate_output(ck.full_text) |
||||
res.add_pretty_table(ck.auxiliary1.pretty_table, ck.auxiliary1.title) |
||||
res.add_pretty_table(ck.auxiliary2.pretty_table, ck.auxiliary2.title) |
||||
res.add_pretty_table(ck.auxiliary3.pretty_table, ck.auxiliary3.title) |
||||
res.add_pretty_table(ck.auxiliary4.pretty_table, ck.auxiliary4.title) |
||||
res.add_pretty_table(ck.core.pretty_table, ck.core.title) |
||||
self.info(ck.full_text) |
||||
|
||||
return res |
@ -0,0 +1,49 @@ |
||||
""" |
||||
Halstead complexity metrics |
||||
https://en.wikipedia.org/wiki/Halstead_complexity_measures |
||||
|
||||
12 metrics based on the number of unique operators and operands: |
||||
|
||||
Core metrics: |
||||
n1 = the number of distinct operators |
||||
n2 = the number of distinct operands |
||||
N1 = the total number of operators |
||||
N2 = the total number of operands |
||||
|
||||
Extended metrics1: |
||||
n = n1 + n2 # Program vocabulary |
||||
N = N1 + N2 # Program length |
||||
S = n1 * log2(n1) + n2 * log2(n2) # Estimated program length |
||||
V = N * log2(n) # Volume |
||||
|
||||
Extended metrics2: |
||||
D = (n1 / 2) * (N2 / n2) # Difficulty |
||||
E = D * V # Effort |
||||
T = E / 18 seconds # Time required to program |
||||
B = (E^(2/3)) / 3000 # Number of delivered bugs |
||||
|
||||
""" |
||||
from slither.printers.abstract_printer import AbstractPrinter |
||||
from slither.utils.halstead import HalsteadMetrics |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
class Halstead(AbstractPrinter): |
||||
ARGUMENT = "halstead" |
||||
HELP = "Computes the Halstead complexity metrics for each contract" |
||||
|
||||
WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#halstead" |
||||
|
||||
def output(self, _filename: str) -> Output: |
||||
if len(self.contracts) == 0: |
||||
return self.generate_output("No contract found") |
||||
|
||||
halstead = HalsteadMetrics(self.contracts) |
||||
|
||||
res = self.generate_output(halstead.full_text) |
||||
res.add_pretty_table(halstead.core.pretty_table, halstead.core.title) |
||||
res.add_pretty_table(halstead.extended1.pretty_table, halstead.extended1.title) |
||||
res.add_pretty_table(halstead.extended2.pretty_table, halstead.extended2.title) |
||||
self.info(halstead.full_text) |
||||
|
||||
return res |
@ -0,0 +1,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,32 @@ |
||||
""" |
||||
Robert "Uncle Bob" Martin - Agile software metrics |
||||
https://en.wikipedia.org/wiki/Software_package_metrics |
||||
|
||||
Efferent Coupling (Ce): Number of contracts that the contract depends on |
||||
Afferent Coupling (Ca): Number of contracts that depend on a contract |
||||
Instability (I): Ratio of efferent coupling to total coupling (Ce / (Ce + Ca)) |
||||
Abstractness (A): Number of abstract contracts / total number of contracts |
||||
Distance from the Main Sequence (D): abs(A + I - 1) |
||||
|
||||
""" |
||||
from slither.printers.abstract_printer import AbstractPrinter |
||||
from slither.utils.martin import MartinMetrics |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
class Martin(AbstractPrinter): |
||||
ARGUMENT = "martin" |
||||
HELP = "Martin agile software metrics (Ca, Ce, I, A, D)" |
||||
|
||||
WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#martin" |
||||
|
||||
def output(self, _filename: str) -> Output: |
||||
if len(self.contracts) == 0: |
||||
return self.generate_output("No contract found") |
||||
|
||||
martin = MartinMetrics(self.contracts) |
||||
|
||||
res = self.generate_output(martin.full_text) |
||||
res.add_pretty_table(martin.core.pretty_table, martin.core.title) |
||||
self.info(martin.full_text) |
||||
return res |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue