mirror of https://github.com/crytic/slither
- boolean-cst - tautology - boolean-equal - divide-before-multiplypull/396/head
parent
1b15a401c9
commit
81b3032499
@ -0,0 +1,79 @@ |
||||
""" |
||||
Module detecting misuse of Boolean constants |
||||
""" |
||||
|
||||
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification |
||||
from slither.slithir.operations import Assignment, Call, Return, InitArray, Binary, BinaryType |
||||
from slither.slithir.variables import Constant |
||||
|
||||
|
||||
class BooleanEquality(AbstractDetector): |
||||
""" |
||||
Boolean constant misuse |
||||
""" |
||||
|
||||
ARGUMENT = 'boolean-equal' |
||||
HELP = 'Comparison to boolean constant' |
||||
IMPACT = DetectorClassification.INFORMATIONAL |
||||
CONFIDENCE = DetectorClassification.HIGH |
||||
|
||||
WIKI = 'https://github.com/trailofbits/slither-private/wiki/Vulnerabilities-Description#boolean-equality' |
||||
|
||||
WIKI_TITLE = 'Boolean Equality' |
||||
WIKI_DESCRIPTION = '''Detects the comparison to boolean constant.''' |
||||
WIKI_EXPLOIT_SCENARIO = ''' |
||||
```solidity |
||||
contract A { |
||||
function f(bool x) public { |
||||
// ... |
||||
if (x == true) { // bad! |
||||
// ... |
||||
} |
||||
// ... |
||||
} |
||||
} |
||||
``` |
||||
Boolean can be used directly and do not need to be compare to `true` or `false`.''' |
||||
|
||||
WIKI_RECOMMENDATION = '''Remove the equality to the boolean constant.''' |
||||
|
||||
@staticmethod |
||||
def _detect_boolean_equality(contract): |
||||
|
||||
# Create our result set. |
||||
results = [] |
||||
|
||||
# Loop for each function and modifier. |
||||
for function in contract.functions_and_modifiers_declared: |
||||
f_results = set() |
||||
|
||||
# Loop for every node in this function, looking for boolean constants |
||||
for node in function.nodes: |
||||
for ir in node.irs: |
||||
if isinstance(ir, Binary): |
||||
if ir.type in [BinaryType.EQUAL, BinaryType.NOT_EQUAL]: |
||||
for r in ir.read: |
||||
if isinstance(r, Constant): |
||||
if type(r.value) is bool: |
||||
f_results.add(node) |
||||
results.append((function, f_results)) |
||||
|
||||
# Return the resulting set of nodes with improper uses of Boolean constants |
||||
return results |
||||
|
||||
def _detect(self): |
||||
""" |
||||
Detect Boolean constant misuses |
||||
""" |
||||
results = [] |
||||
for contract in self.contracts: |
||||
boolean_constant_misuses = self._detect_boolean_equality(contract) |
||||
if boolean_constant_misuses: |
||||
for (func, nodes) in boolean_constant_misuses: |
||||
for node in nodes: |
||||
info = [func, " compares to a boolean constant:\n\t-", node, "\n"] |
||||
|
||||
res = self.generate_result(info) |
||||
results.append(res) |
||||
|
||||
return results |
@ -0,0 +1,96 @@ |
||||
""" |
||||
Module detecting misuse of Boolean constants |
||||
""" |
||||
|
||||
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification |
||||
from slither.slithir.operations import Assignment, Call, Return, InitArray, Binary, BinaryType |
||||
from slither.slithir.variables import Constant |
||||
|
||||
|
||||
class BooleanConstantMisuse(AbstractDetector): |
||||
""" |
||||
Boolean constant misuse |
||||
""" |
||||
|
||||
ARGUMENT = 'boolean-cst' |
||||
HELP = 'Misuse of Boolean constant' |
||||
IMPACT = DetectorClassification.MEDIUM |
||||
CONFIDENCE = DetectorClassification.MEDIUM |
||||
|
||||
WIKI = 'https://github.com/trailofbits/slither-private/wiki/Vulnerabilities-Description#misuse-of-a-boolean-constant' |
||||
|
||||
WIKI_TITLE = 'Misuse of a Boolean constant' |
||||
WIKI_DESCRIPTION = '''Detects the misuse of a Boolean constant.''' |
||||
WIKI_EXPLOIT_SCENARIO = ''' |
||||
```solidity |
||||
contract A { |
||||
function f(uint x) public { |
||||
// ... |
||||
if (false) { // bad! |
||||
// ... |
||||
} |
||||
// ... |
||||
} |
||||
|
||||
function g(bool b) public returns (bool) { |
||||
// ... |
||||
return (b || true); // bad! |
||||
// ... |
||||
} |
||||
} |
||||
``` |
||||
Boolean constants in code have only a few legitimate uses. Other uses (in complex expressions, as conditionals) indicate either an error or (most likely) the persistence of debugging/development code that is likely faulty.''' |
||||
|
||||
WIKI_RECOMMENDATION = '''Verify and simplify the condition.''' |
||||
|
||||
@staticmethod |
||||
def _detect_boolean_constant_misuses(contract): |
||||
""" |
||||
Detects and returns all nodes which misuse a Boolean constant. |
||||
:param contract: Contract to detect assignment within. |
||||
:return: A list of misusing nodes. |
||||
""" |
||||
|
||||
# Create our result set. |
||||
results = [] |
||||
|
||||
# Loop for each function and modifier. |
||||
for function in contract.functions_declared: |
||||
f_results = set() |
||||
|
||||
# Loop for every node in this function, looking for boolean constants |
||||
for node in function.nodes: |
||||
for ir in node.irs: |
||||
if isinstance(ir, (Assignment, Call, Return, InitArray)): |
||||
# It's ok to use a bare boolean constant in these contexts |
||||
continue |
||||
if isinstance(ir, Binary): |
||||
if ir.type in [BinaryType.ADDITION, BinaryType.EQUAL, BinaryType.NOT_EQUAL]: |
||||
# Comparing to a Boolean constant is dubious style, but harmless |
||||
# Equal is catch by another detector (informational severity) |
||||
continue |
||||
for r in ir.read: |
||||
if isinstance(r, Constant): |
||||
if type(r.value) is bool: |
||||
f_results.add(node) |
||||
results.append((function, f_results)) |
||||
|
||||
# Return the resulting set of nodes with improper uses of Boolean constants |
||||
return results |
||||
|
||||
def _detect(self): |
||||
""" |
||||
Detect Boolean constant misuses |
||||
""" |
||||
results = [] |
||||
for contract in self.contracts: |
||||
boolean_constant_misuses = self._detect_boolean_constant_misuses(contract) |
||||
if boolean_constant_misuses: |
||||
for (func, nodes) in boolean_constant_misuses: |
||||
for node in nodes: |
||||
info = [func, " uses a Boolean constant improperly:\n\t-", node, "\n"] |
||||
|
||||
res = self.generate_result(info) |
||||
results.append(res) |
||||
|
||||
return results |
@ -0,0 +1,179 @@ |
||||
""" |
||||
Module detecting possible loss of precision due to divide before multiple |
||||
""" |
||||
from collections import defaultdict |
||||
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification |
||||
from slither.slithir.operations import Binary, Assignment, BinaryType, LibraryCall |
||||
from slither.slithir.variables import Constant |
||||
|
||||
|
||||
def is_division(ir): |
||||
if isinstance(ir, Binary): |
||||
if ir.type == BinaryType.DIVISION: |
||||
return True |
||||
|
||||
if isinstance(ir, LibraryCall): |
||||
if ir.function.name.lower() in ['div', 'safediv', ]: |
||||
if len(ir.arguments) == 2: |
||||
if ir.lvalue: |
||||
return True |
||||
return False |
||||
|
||||
|
||||
def is_multiplication(ir): |
||||
if isinstance(ir, Binary): |
||||
if ir.type == BinaryType.MULTIPLICATION: |
||||
return True |
||||
|
||||
if isinstance(ir, LibraryCall): |
||||
if ir.function.name.lower() in ['mul', 'safemul', ]: |
||||
if len(ir.arguments) == 2: |
||||
if ir.lvalue: |
||||
return True |
||||
return False |
||||
|
||||
|
||||
def is_assert(node): |
||||
if node.contains_require_or_assert(): |
||||
return True |
||||
# Old Solidity code where using an internal 'assert(bool)' function |
||||
# While we dont check that this function is correct, we assume it is |
||||
# To avoid too many FP |
||||
if "assert(bool)" in [c.full_name for c in node.internal_calls]: |
||||
return True |
||||
return False |
||||
|
||||
|
||||
class DivideBeforeMultiply(AbstractDetector): |
||||
""" |
||||
Divide before multiply |
||||
""" |
||||
|
||||
ARGUMENT = 'divide-before-multiply' |
||||
HELP = 'Imprecise arithmetic operations order' |
||||
IMPACT = DetectorClassification.MEDIUM |
||||
CONFIDENCE = DetectorClassification.MEDIUM |
||||
|
||||
WIKI = 'https://github.com/trailofbits/slither-private/wiki/Vulnerabilities-Description#divide-before-multiply' |
||||
|
||||
WIKI_TITLE = 'Divide before multiply' |
||||
WIKI_DESCRIPTION = '''Solidity only supports integers, so division will often truncate; performing a multiply before a divison can sometimes avoid loss of precision.''' |
||||
WIKI_DESCRIPTION = '''Solidity integeer division will might truncate. As a result, performing a multiply before a divison might lead to loss of precision.''' |
||||
WIKI_EXPLOIT_SCENARIO = ''' |
||||
```solidity |
||||
contract A { |
||||
function f(uint n) public { |
||||
coins = (oldSupply / n) * interest; |
||||
} |
||||
} |
||||
``` |
||||
If `n` is greater than `oldSupply`, `coins` will be zero. For example, with `oldSupply = 5; n = 10, interest = 2`, coins will be zero. |
||||
If `(oldSupply * interest / n)` was used, `coins` would have been `1`. |
||||
In general, it's usually a good idea to re-arrange arithmetic to perform multiply before divide, unless the limit of a smaller type makes this dangerous.''' |
||||
|
||||
WIKI_RECOMMENDATION = '''Consider ordering multiplication prior division.''' |
||||
|
||||
def _explore(self, node, explored, f_results, divisions): |
||||
if node in explored: |
||||
return |
||||
explored.add(node) |
||||
|
||||
equality_found = False |
||||
# List of nodes related to one bug instance |
||||
node_results = [] |
||||
|
||||
for ir in node.irs: |
||||
# check for Constant, has its not hashable (TODO: make Constant hashable) |
||||
if isinstance(ir, Assignment) and not isinstance(ir.rvalue, Constant): |
||||
if ir.rvalue in divisions: |
||||
# Avoid dupplicate. We dont use set so we keep the order of the nodes |
||||
if node not in divisions[ir.rvalue]: |
||||
divisions[ir.lvalue] = divisions[ir.rvalue] + [node] |
||||
else: |
||||
divisions[ir.lvalue] = divisions[ir.rvalue] |
||||
|
||||
if is_division(ir): |
||||
divisions[ir.lvalue] = [node] |
||||
|
||||
if is_multiplication(ir): |
||||
mul_arguments = ir.read if isinstance(ir, Binary) else ir.arguments |
||||
nodes = [] |
||||
for r in mul_arguments: |
||||
if not isinstance(r, Constant) and (r in divisions): |
||||
# Dont add node already present to avoid dupplicate |
||||
# We dont use set to keep the order of the nodes |
||||
if node in divisions[r]: |
||||
nodes += [n for n in divisions[r] if n not in nodes] |
||||
else: |
||||
nodes += [n for n in divisions[r] + [node] if n not in nodes] |
||||
if nodes: |
||||
node_results = nodes |
||||
|
||||
if isinstance(ir, Binary) and ir.type == BinaryType.EQUAL: |
||||
equality_found = True |
||||
|
||||
if node_results: |
||||
# We do not track the case where the multiplication is done in a require() or assert() |
||||
# Which also contains a ==, to prevent FP due to the form |
||||
# assert(a == b * c + a % b) |
||||
if is_assert(node) and equality_found: |
||||
pass |
||||
else: |
||||
f_results.append(node_results) |
||||
|
||||
for son in node.sons: |
||||
self._explore(son, explored, f_results, divisions) |
||||
|
||||
def detect_divide_before_multiply(self, contract): |
||||
""" |
||||
Detects and returns all nodes with multiplications of division results. |
||||
:param contract: Contract to detect assignment within. |
||||
:return: A list of nodes with multiplications of divisions. |
||||
""" |
||||
|
||||
# Create our result set. |
||||
# List of tuple (function -> list(list(nodes))) |
||||
# Each list(nodes) of the list is one bug instances |
||||
# Each node in the list(nodes) is involved in the bug |
||||
results = [] |
||||
|
||||
# Loop for each function and modifier. |
||||
for function in contract.functions_declared: |
||||
if not function.entry_point: |
||||
continue |
||||
|
||||
# List of list(nodes) |
||||
# Each list(nodes) is one bug instances |
||||
f_results = [] |
||||
|
||||
# lvalue -> node |
||||
# track all the division results (and the assignment of the division results) |
||||
divisions = defaultdict(list) |
||||
|
||||
self._explore(function.entry_point, set(), f_results, divisions) |
||||
|
||||
for f_result in f_results: |
||||
results.append((function, f_result)) |
||||
|
||||
# Return the resulting set of nodes with divisions before multiplications |
||||
return results |
||||
|
||||
def _detect(self): |
||||
""" |
||||
Detect divisions before multiplications |
||||
""" |
||||
results = [] |
||||
for contract in self.contracts: |
||||
divisions_before_multiplications = self.detect_divide_before_multiply(contract) |
||||
if divisions_before_multiplications: |
||||
for (func, nodes) in divisions_before_multiplications: |
||||
|
||||
info = [func, " performs a multiplication on the result of a division:\n"] |
||||
|
||||
for node in nodes: |
||||
info += ["\t-", node, "\n"] |
||||
|
||||
res = self.generate_result(info) |
||||
results.append(res) |
||||
|
||||
return results |
@ -0,0 +1,158 @@ |
||||
""" |
||||
Module detecting tautologies and contradictions based on types in comparison operations over integers |
||||
""" |
||||
|
||||
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification |
||||
from slither.slithir.operations import Binary, BinaryType |
||||
from slither.slithir.variables import Constant |
||||
from slither.core.solidity_types.elementary_type import Int, Uint |
||||
|
||||
class TypeBasedTautology(AbstractDetector): |
||||
""" |
||||
Type-based tautology or contradiction |
||||
""" |
||||
|
||||
ARGUMENT = 'tautology' |
||||
HELP = 'Tautology or contradiction' |
||||
IMPACT = DetectorClassification.MEDIUM |
||||
CONFIDENCE = DetectorClassification.HIGH |
||||
|
||||
WIKI = 'https://github.com/trailofbits/slither-private/wiki/Vulnerabilities-Description#tautology-or-contradiction' |
||||
|
||||
WIKI_TITLE = 'Tautology or contradiction' |
||||
WIKI_DESCRIPTION = '''Detects expressions that are tautologies or contradictions.''' |
||||
WIKI_EXPLOIT_SCENARIO = ''' |
||||
```solidity |
||||
contract A { |
||||
function f(uint x) public { |
||||
// ... |
||||
if (x >= 0) { // bad -- always true |
||||
// ... |
||||
} |
||||
// ... |
||||
} |
||||
|
||||
function g(uint8 y) public returns (bool) { |
||||
// ... |
||||
return (y < 512); // bad! |
||||
// ... |
||||
} |
||||
} |
||||
``` |
||||
`x` is an `uint256`, as a result `x >= 0` will be always true. |
||||
`y` is an `uint8`, as a result `y <512` will be always true. |
||||
''' |
||||
|
||||
WIKI_RECOMMENDATION = '''Fix the incorrect comparison by chaning the value type or the comparison.''' |
||||
|
||||
def typeRange(self, t): |
||||
bits = int(t.split("int")[1]) |
||||
if t in Uint: |
||||
return (0, (2**bits)-1) |
||||
if t in Int: |
||||
v = (2**(bits-1))-1 |
||||
return (-v, v) |
||||
|
||||
|
||||
flip_table = { |
||||
BinaryType.GREATER: BinaryType.LESS, |
||||
BinaryType.GREATER_EQUAL: BinaryType.LESS_EQUAL, |
||||
BinaryType.LESS: BinaryType.GREATER, |
||||
BinaryType.LESS_EQUAL: BinaryType.GREATER_EQUAL, |
||||
} |
||||
|
||||
def _detect_tautology_or_contradiction(self, low, high, cval, op): |
||||
''' |
||||
Return true if "[low high] op cval " is always true or always false |
||||
:param low: |
||||
:param high: |
||||
:param cval: |
||||
:param op: |
||||
:return: |
||||
''' |
||||
if op == BinaryType.LESS: |
||||
# a < cval |
||||
# its a tautology if |
||||
# high(a) < cval |
||||
# its a contradiction if |
||||
# low(a) >= cval |
||||
return high < cval or low >= cval |
||||
elif op == BinaryType.GREATER: |
||||
# a > cval |
||||
# its a tautology if |
||||
# low(a) > cval |
||||
# its a contradiction if |
||||
# high(a) <= cval |
||||
return low > cval or high <= cval |
||||
elif op == BinaryType.LESS_EQUAL: |
||||
# a <= cval |
||||
# its a tautology if |
||||
# high(a) <= cval |
||||
# its a contradiction if |
||||
# low(a) > cval |
||||
return (high <= cval) or (low > cval) |
||||
elif op == BinaryType.GREATER_EQUAL: |
||||
# a >= cval |
||||
# its a tautology if |
||||
# low(a) >= cval |
||||
# its a contradiction if |
||||
# high(a) < cval |
||||
return (low >= cval) or (high < cval) |
||||
return False |
||||
|
||||
def detect_type_based_tautologies(self, contract): |
||||
""" |
||||
Detects and returns all nodes with tautology/contradiction comparisons (based on type alone). |
||||
:param contract: Contract to detect assignment within. |
||||
:return: A list of nodes with tautolgies/contradictions. |
||||
""" |
||||
|
||||
# Create our result set. |
||||
results = [] |
||||
allInts = Int + Uint |
||||
|
||||
# Loop for each function and modifier. |
||||
for function in contract.functions_declared: |
||||
f_results = set() |
||||
|
||||
for node in function.nodes: |
||||
for ir in node.irs: |
||||
if isinstance(ir, Binary) and ir.type in self.flip_table: |
||||
# If neither side is a constant, we can't do much |
||||
if isinstance(ir.variable_left, Constant): |
||||
cval = ir.variable_left.value |
||||
rtype = str(ir.variable_right.type) |
||||
if rtype in allInts: |
||||
(low, high) = self.typeRange(rtype) |
||||
if self._detect_tautology_or_contradiction(low, high, cval, self.flip_table[ir.type]): |
||||
f_results.add(node) |
||||
|
||||
if isinstance(ir.variable_right, Constant): |
||||
cval = ir.variable_right.value |
||||
ltype = str(ir.variable_left.type) |
||||
if ltype in allInts: |
||||
(low, high) = self.typeRange(ltype) |
||||
if self._detect_tautology_or_contradiction(low, high, cval, ir.type): |
||||
f_results.add(node) |
||||
results.append((function, f_results)) |
||||
|
||||
# Return the resulting set of nodes with tautologies and contradictions |
||||
return results |
||||
|
||||
def _detect(self): |
||||
""" |
||||
Detect tautological (or contradictory) comparisons |
||||
""" |
||||
results = [] |
||||
for contract in self.contracts: |
||||
tautologies = self.detect_type_based_tautologies(contract) |
||||
if tautologies: |
||||
for (func, nodes) in tautologies: |
||||
for node in nodes: |
||||
info = [func, " contains a tautology or contradiction:\n"] |
||||
info += ["\t- ", node, "\n"] |
||||
|
||||
res = self.generate_result(info) |
||||
results.append(res) |
||||
|
||||
return results |
Loading…
Reference in new issue