mirror of https://github.com/crytic/slither
Merge pull request #1757 from webthethird/dev-upgradeability-utils
Add upgradeability utilspull/1817/head
commit
8b2fd9adf7
@ -0,0 +1,580 @@ |
|||||||
|
from typing import Optional, Tuple, List, Union |
||||||
|
from slither.core.declarations import ( |
||||||
|
Contract, |
||||||
|
Structure, |
||||||
|
Enum, |
||||||
|
SolidityVariableComposed, |
||||||
|
SolidityVariable, |
||||||
|
Function, |
||||||
|
) |
||||||
|
from slither.core.solidity_types import ( |
||||||
|
Type, |
||||||
|
ElementaryType, |
||||||
|
ArrayType, |
||||||
|
MappingType, |
||||||
|
UserDefinedType, |
||||||
|
) |
||||||
|
from slither.core.variables.local_variable import LocalVariable |
||||||
|
from slither.core.variables.local_variable_init_from_tuple import LocalVariableInitFromTuple |
||||||
|
from slither.core.variables.state_variable import StateVariable |
||||||
|
from slither.analyses.data_dependency.data_dependency import get_dependencies |
||||||
|
from slither.core.variables.variable import Variable |
||||||
|
from slither.core.expressions.literal import Literal |
||||||
|
from slither.core.expressions.identifier import Identifier |
||||||
|
from slither.core.expressions.call_expression import CallExpression |
||||||
|
from slither.core.expressions.assignment_operation import AssignmentOperation |
||||||
|
from slither.core.cfg.node import Node, NodeType |
||||||
|
from slither.slithir.operations import ( |
||||||
|
Operation, |
||||||
|
Assignment, |
||||||
|
Index, |
||||||
|
Member, |
||||||
|
Length, |
||||||
|
Binary, |
||||||
|
Unary, |
||||||
|
Condition, |
||||||
|
NewArray, |
||||||
|
NewStructure, |
||||||
|
NewContract, |
||||||
|
NewElementaryType, |
||||||
|
SolidityCall, |
||||||
|
Delete, |
||||||
|
EventCall, |
||||||
|
LibraryCall, |
||||||
|
InternalDynamicCall, |
||||||
|
HighLevelCall, |
||||||
|
LowLevelCall, |
||||||
|
TypeConversion, |
||||||
|
Return, |
||||||
|
Transfer, |
||||||
|
Send, |
||||||
|
Unpack, |
||||||
|
InitArray, |
||||||
|
InternalCall, |
||||||
|
) |
||||||
|
from slither.slithir.variables import ( |
||||||
|
TemporaryVariable, |
||||||
|
TupleVariable, |
||||||
|
Constant, |
||||||
|
ReferenceVariable, |
||||||
|
) |
||||||
|
from slither.tools.read_storage.read_storage import SlotInfo, SlitherReadStorage |
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-locals |
||||||
|
def compare( |
||||||
|
v1: Contract, v2: Contract |
||||||
|
) -> Tuple[ |
||||||
|
List[Variable], List[Variable], List[Variable], List[Function], List[Function], List[Function] |
||||||
|
]: |
||||||
|
""" |
||||||
|
Compares two versions of a contract. Most useful for upgradeable (logic) contracts, |
||||||
|
but does not require that Contract.is_upgradeable returns true for either contract. |
||||||
|
|
||||||
|
Args: |
||||||
|
v1: Original version of (upgradeable) contract |
||||||
|
v2: Updated version of (upgradeable) contract |
||||||
|
|
||||||
|
Returns: |
||||||
|
missing-vars-in-v2: list[Variable], |
||||||
|
new-variables: list[Variable], |
||||||
|
tainted-variables: list[Variable], |
||||||
|
new-functions: list[Function], |
||||||
|
modified-functions: list[Function], |
||||||
|
tainted-functions: list[Function] |
||||||
|
""" |
||||||
|
|
||||||
|
order_vars1 = [ |
||||||
|
v for v in v1.state_variables_ordered if not v.is_constant and not v.is_immutable |
||||||
|
] |
||||||
|
order_vars2 = [ |
||||||
|
v for v in v2.state_variables_ordered if not v.is_constant and not v.is_immutable |
||||||
|
] |
||||||
|
func_sigs1 = [function.solidity_signature for function in v1.functions] |
||||||
|
func_sigs2 = [function.solidity_signature for function in v2.functions] |
||||||
|
|
||||||
|
missing_vars_in_v2 = [] |
||||||
|
new_variables = [] |
||||||
|
tainted_variables = [] |
||||||
|
new_functions = [] |
||||||
|
modified_functions = [] |
||||||
|
tainted_functions = [] |
||||||
|
|
||||||
|
# Since this is not a detector, include any missing variables in the v2 contract |
||||||
|
if len(order_vars2) < len(order_vars1): |
||||||
|
missing_vars_in_v2.extend(get_missing_vars(v1, v2)) |
||||||
|
|
||||||
|
# Find all new and modified functions in the v2 contract |
||||||
|
new_modified_functions = [] |
||||||
|
new_modified_function_vars = [] |
||||||
|
for sig in func_sigs2: |
||||||
|
function = v2.get_function_from_signature(sig) |
||||||
|
orig_function = v1.get_function_from_signature(sig) |
||||||
|
if sig not in func_sigs1: |
||||||
|
new_modified_functions.append(function) |
||||||
|
new_functions.append(function) |
||||||
|
new_modified_function_vars += ( |
||||||
|
function.state_variables_read + function.state_variables_written |
||||||
|
) |
||||||
|
elif not function.is_constructor_variables and is_function_modified( |
||||||
|
orig_function, function |
||||||
|
): |
||||||
|
new_modified_functions.append(function) |
||||||
|
modified_functions.append(function) |
||||||
|
new_modified_function_vars += ( |
||||||
|
function.state_variables_read + function.state_variables_written |
||||||
|
) |
||||||
|
|
||||||
|
# Find all unmodified functions that call a modified function or read/write the |
||||||
|
# same state variable(s) as a new/modified function, i.e., tainted functions |
||||||
|
for function in v2.functions: |
||||||
|
if ( |
||||||
|
function in new_modified_functions |
||||||
|
or function.is_constructor |
||||||
|
or function.name.startswith("slither") |
||||||
|
): |
||||||
|
continue |
||||||
|
modified_calls = [ |
||||||
|
func for func in new_modified_functions if func in function.internal_calls |
||||||
|
] |
||||||
|
tainted_vars = [ |
||||||
|
var |
||||||
|
for var in set(new_modified_function_vars) |
||||||
|
if var in function.variables_read_or_written |
||||||
|
and not var.is_constant |
||||||
|
and not var.is_immutable |
||||||
|
] |
||||||
|
if len(modified_calls) > 0 or len(tainted_vars) > 0: |
||||||
|
tainted_functions.append(function) |
||||||
|
|
||||||
|
# Find all new or tainted variables, i.e., variables that are read or written by a new/modified/tainted function |
||||||
|
for var in order_vars2: |
||||||
|
read_by = v2.get_functions_reading_from_variable(var) |
||||||
|
written_by = v2.get_functions_writing_to_variable(var) |
||||||
|
if v1.get_state_variable_from_name(var.name) is None: |
||||||
|
new_variables.append(var) |
||||||
|
elif any( |
||||||
|
func in read_by or func in written_by |
||||||
|
for func in new_modified_functions + tainted_functions |
||||||
|
): |
||||||
|
tainted_variables.append(var) |
||||||
|
|
||||||
|
return ( |
||||||
|
missing_vars_in_v2, |
||||||
|
new_variables, |
||||||
|
tainted_variables, |
||||||
|
new_functions, |
||||||
|
modified_functions, |
||||||
|
tainted_functions, |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
def get_missing_vars(v1: Contract, v2: Contract) -> List[StateVariable]: |
||||||
|
""" |
||||||
|
Gets all non-constant/immutable StateVariables that appear in v1 but not v2 |
||||||
|
Args: |
||||||
|
v1: Contract version 1 |
||||||
|
v2: Contract version 2 |
||||||
|
|
||||||
|
Returns: |
||||||
|
List of StateVariables from v1 missing in v2 |
||||||
|
""" |
||||||
|
results = [] |
||||||
|
order_vars1 = [ |
||||||
|
v for v in v1.state_variables_ordered if not v.is_constant and not v.is_immutable |
||||||
|
] |
||||||
|
order_vars2 = [ |
||||||
|
v for v in v2.state_variables_ordered if not v.is_constant and not v.is_immutable |
||||||
|
] |
||||||
|
if len(order_vars2) < len(order_vars1): |
||||||
|
for variable in order_vars1: |
||||||
|
if variable.name not in [v.name for v in order_vars2]: |
||||||
|
results.append(variable) |
||||||
|
return results |
||||||
|
|
||||||
|
|
||||||
|
def is_function_modified(f1: Function, f2: Function) -> bool: |
||||||
|
""" |
||||||
|
Compares two versions of a function, and returns True if the function has been modified. |
||||||
|
First checks whether the functions' content hashes are equal to quickly rule out identical functions. |
||||||
|
Walks the CFGs and compares IR operations if hashes differ to rule out false positives, i.e., from changed comments. |
||||||
|
|
||||||
|
Args: |
||||||
|
f1: Original version of the function |
||||||
|
f2: New version of the function |
||||||
|
|
||||||
|
Returns: |
||||||
|
True if the functions differ, otherwise False |
||||||
|
""" |
||||||
|
# If the function content hashes are the same, no need to investigate the function further |
||||||
|
if f1.source_mapping.content_hash == f2.source_mapping.content_hash: |
||||||
|
return False |
||||||
|
# If the hashes differ, it is possible a change in a name or in a comment could be the only difference |
||||||
|
# So we need to resort to walking through the CFG and comparing the IR operations |
||||||
|
queue_f1 = [f1.entry_point] |
||||||
|
queue_f2 = [f2.entry_point] |
||||||
|
visited = [] |
||||||
|
while len(queue_f1) > 0 and len(queue_f2) > 0: |
||||||
|
node_f1 = queue_f1.pop(0) |
||||||
|
node_f2 = queue_f2.pop(0) |
||||||
|
visited.extend([node_f1, node_f2]) |
||||||
|
queue_f1.extend(son for son in node_f1.sons if son not in visited) |
||||||
|
queue_f2.extend(son for son in node_f2.sons if son not in visited) |
||||||
|
for i, ir in enumerate(node_f1.irs): |
||||||
|
if encode_ir_for_compare(ir) != encode_ir_for_compare(node_f2.irs[i]): |
||||||
|
return True |
||||||
|
return False |
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-branches |
||||||
|
def ntype(_type: Union[Type, str]) -> str: |
||||||
|
if isinstance(_type, ElementaryType): |
||||||
|
_type = str(_type) |
||||||
|
elif isinstance(_type, ArrayType): |
||||||
|
if isinstance(_type.type, ElementaryType): |
||||||
|
_type = str(_type) |
||||||
|
else: |
||||||
|
_type = "user_defined_array" |
||||||
|
elif isinstance(_type, Structure): |
||||||
|
_type = str(_type) |
||||||
|
elif isinstance(_type, Enum): |
||||||
|
_type = str(_type) |
||||||
|
elif isinstance(_type, MappingType): |
||||||
|
_type = str(_type) |
||||||
|
elif isinstance(_type, UserDefinedType): |
||||||
|
if isinstance(_type.type, Contract): |
||||||
|
_type = f"contract({_type.type.name})" |
||||||
|
elif isinstance(_type.type, Structure): |
||||||
|
_type = f"struct({_type.type.name})" |
||||||
|
elif isinstance(_type.type, Enum): |
||||||
|
_type = f"enum({_type.type.name})" |
||||||
|
else: |
||||||
|
_type = str(_type) |
||||||
|
|
||||||
|
_type = _type.replace(" memory", "") |
||||||
|
_type = _type.replace(" storage ref", "") |
||||||
|
|
||||||
|
if "struct" in _type: |
||||||
|
return "struct" |
||||||
|
if "enum" in _type: |
||||||
|
return "enum" |
||||||
|
if "tuple" in _type: |
||||||
|
return "tuple" |
||||||
|
if "contract" in _type: |
||||||
|
return "contract" |
||||||
|
if "mapping" in _type: |
||||||
|
return "mapping" |
||||||
|
return _type.replace(" ", "_") |
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-branches |
||||||
|
def encode_ir_for_compare(ir: Operation) -> str: |
||||||
|
# operations |
||||||
|
if isinstance(ir, Assignment): |
||||||
|
return f"({encode_var_for_compare(ir.lvalue)}):=({encode_var_for_compare(ir.rvalue)})" |
||||||
|
if isinstance(ir, Index): |
||||||
|
return f"index({ntype(ir.index_type)})" |
||||||
|
if isinstance(ir, Member): |
||||||
|
return "member" # .format(ntype(ir._type)) |
||||||
|
if isinstance(ir, Length): |
||||||
|
return "length" |
||||||
|
if isinstance(ir, Binary): |
||||||
|
return f"binary({str(ir.variable_left)}{str(ir.type)}{str(ir.variable_right)})" |
||||||
|
if isinstance(ir, Unary): |
||||||
|
return f"unary({str(ir.type)})" |
||||||
|
if isinstance(ir, Condition): |
||||||
|
return f"condition({encode_var_for_compare(ir.value)})" |
||||||
|
if isinstance(ir, NewStructure): |
||||||
|
return "new_structure" |
||||||
|
if isinstance(ir, NewContract): |
||||||
|
return "new_contract" |
||||||
|
if isinstance(ir, NewArray): |
||||||
|
return f"new_array({ntype(ir.array_type)})" |
||||||
|
if isinstance(ir, NewElementaryType): |
||||||
|
return f"new_elementary({ntype(ir.type)})" |
||||||
|
if isinstance(ir, Delete): |
||||||
|
return f"delete({encode_var_for_compare(ir.lvalue)},{encode_var_for_compare(ir.variable)})" |
||||||
|
if isinstance(ir, SolidityCall): |
||||||
|
return f"solidity_call({ir.function.full_name})" |
||||||
|
if isinstance(ir, InternalCall): |
||||||
|
return f"internal_call({ntype(ir.type_call)})" |
||||||
|
if isinstance(ir, EventCall): # is this useful? |
||||||
|
return "event" |
||||||
|
if isinstance(ir, LibraryCall): |
||||||
|
return "library_call" |
||||||
|
if isinstance(ir, InternalDynamicCall): |
||||||
|
return "internal_dynamic_call" |
||||||
|
if isinstance(ir, HighLevelCall): # TODO: improve |
||||||
|
return "high_level_call" |
||||||
|
if isinstance(ir, LowLevelCall): # TODO: improve |
||||||
|
return "low_level_call" |
||||||
|
if isinstance(ir, TypeConversion): |
||||||
|
return f"type_conversion({ntype(ir.type)})" |
||||||
|
if isinstance(ir, Return): # this can be improved using values |
||||||
|
return "return" # .format(ntype(ir.type)) |
||||||
|
if isinstance(ir, Transfer): |
||||||
|
return f"transfer({encode_var_for_compare(ir.call_value)})" |
||||||
|
if isinstance(ir, Send): |
||||||
|
return f"send({encode_var_for_compare(ir.call_value)})" |
||||||
|
if isinstance(ir, Unpack): # TODO: improve |
||||||
|
return "unpack" |
||||||
|
if isinstance(ir, InitArray): # TODO: improve |
||||||
|
return "init_array" |
||||||
|
|
||||||
|
# default |
||||||
|
return "" |
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-branches |
||||||
|
def encode_var_for_compare(var: Variable) -> str: |
||||||
|
|
||||||
|
# variables |
||||||
|
if isinstance(var, Constant): |
||||||
|
return f"constant({ntype(var.type)})" |
||||||
|
if isinstance(var, SolidityVariableComposed): |
||||||
|
return f"solidity_variable_composed({var.name})" |
||||||
|
if isinstance(var, SolidityVariable): |
||||||
|
return f"solidity_variable{var.name}" |
||||||
|
if isinstance(var, TemporaryVariable): |
||||||
|
return "temporary_variable" |
||||||
|
if isinstance(var, ReferenceVariable): |
||||||
|
return f"reference({ntype(var.type)})" |
||||||
|
if isinstance(var, LocalVariable): |
||||||
|
return f"local_solc_variable({var.location})" |
||||||
|
if isinstance(var, StateVariable): |
||||||
|
return f"state_solc_variable({ntype(var.type)})" |
||||||
|
if isinstance(var, LocalVariableInitFromTuple): |
||||||
|
return "local_variable_init_tuple" |
||||||
|
if isinstance(var, TupleVariable): |
||||||
|
return "tuple_variable" |
||||||
|
|
||||||
|
# default |
||||||
|
return "" |
||||||
|
|
||||||
|
|
||||||
|
def get_proxy_implementation_slot(proxy: Contract) -> Optional[SlotInfo]: |
||||||
|
""" |
||||||
|
Gets information about the storage slot where a proxy's implementation address is stored. |
||||||
|
Args: |
||||||
|
proxy: A Contract object (proxy.is_upgradeable_proxy should be true). |
||||||
|
|
||||||
|
Returns: |
||||||
|
(`SlotInfo`) | None : A dictionary of the slot information. |
||||||
|
""" |
||||||
|
|
||||||
|
delegate = get_proxy_implementation_var(proxy) |
||||||
|
if isinstance(delegate, StateVariable): |
||||||
|
if not delegate.is_constant and not delegate.is_immutable: |
||||||
|
srs = SlitherReadStorage([proxy], 20) |
||||||
|
return srs.get_storage_slot(delegate, proxy) |
||||||
|
if delegate.is_constant and delegate.type.name == "bytes32": |
||||||
|
return SlotInfo( |
||||||
|
name=delegate.name, |
||||||
|
type_string="address", |
||||||
|
slot=int(delegate.expression.value, 16), |
||||||
|
size=160, |
||||||
|
offset=0, |
||||||
|
) |
||||||
|
return None |
||||||
|
|
||||||
|
|
||||||
|
def get_proxy_implementation_var(proxy: Contract) -> Optional[Variable]: |
||||||
|
""" |
||||||
|
Gets the Variable that stores a proxy's implementation address. Uses data dependency to trace any LocalVariable |
||||||
|
that is passed into a delegatecall as the target address back to its data source, ideally a StateVariable. |
||||||
|
Can return a newly created StateVariable if an `sload` from a hardcoded storage slot is found in assembly. |
||||||
|
Args: |
||||||
|
proxy: A Contract object (proxy.is_upgradeable_proxy should be true). |
||||||
|
|
||||||
|
Returns: |
||||||
|
(`Variable`) | None : The variable, ideally a StateVariable, which stores the proxy's implementation address. |
||||||
|
""" |
||||||
|
if not proxy.is_upgradeable_proxy or not proxy.fallback_function: |
||||||
|
return None |
||||||
|
|
||||||
|
delegate = find_delegate_in_fallback(proxy) |
||||||
|
if isinstance(delegate, LocalVariable): |
||||||
|
dependencies = get_dependencies(delegate, proxy) |
||||||
|
try: |
||||||
|
delegate = next(var for var in dependencies if isinstance(var, StateVariable)) |
||||||
|
except StopIteration: |
||||||
|
return delegate |
||||||
|
return delegate |
||||||
|
|
||||||
|
|
||||||
|
def find_delegate_in_fallback(proxy: Contract) -> Optional[Variable]: |
||||||
|
""" |
||||||
|
Searches a proxy's fallback function for a delegatecall, then extracts the Variable being passed in as the target. |
||||||
|
Can return a newly created StateVariable if an `sload` from a hardcoded storage slot is found in assembly. |
||||||
|
Should typically be called by get_proxy_implementation_var(proxy). |
||||||
|
Args: |
||||||
|
proxy: A Contract object (should have a fallback function). |
||||||
|
|
||||||
|
Returns: |
||||||
|
(`Variable`) | None : The variable being passed as the destination argument in a delegatecall in the fallback. |
||||||
|
""" |
||||||
|
delegate: Optional[Variable] = None |
||||||
|
fallback = proxy.fallback_function |
||||||
|
for node in fallback.all_nodes(): |
||||||
|
for ir in node.irs: |
||||||
|
if isinstance(ir, LowLevelCall) and ir.function_name == "delegatecall": |
||||||
|
delegate = ir.destination |
||||||
|
if delegate is not None: |
||||||
|
break |
||||||
|
if ( |
||||||
|
node.type == NodeType.ASSEMBLY |
||||||
|
and isinstance(node.inline_asm, str) |
||||||
|
and "delegatecall" in node.inline_asm |
||||||
|
): |
||||||
|
delegate = extract_delegate_from_asm(proxy, node) |
||||||
|
elif node.type == NodeType.EXPRESSION: |
||||||
|
expression = node.expression |
||||||
|
if isinstance(expression, AssignmentOperation): |
||||||
|
expression = expression.expression_right |
||||||
|
if ( |
||||||
|
isinstance(expression, CallExpression) |
||||||
|
and "delegatecall" in str(expression.called) |
||||||
|
and len(expression.arguments) > 1 |
||||||
|
): |
||||||
|
dest = expression.arguments[1] |
||||||
|
if isinstance(dest, CallExpression) and "sload" in str(dest.called): |
||||||
|
dest = dest.arguments[0] |
||||||
|
if isinstance(dest, Identifier): |
||||||
|
delegate = dest.value |
||||||
|
break |
||||||
|
if ( |
||||||
|
isinstance(dest, Literal) and len(dest.value) == 66 |
||||||
|
): # 32 bytes = 64 chars + "0x" = 66 chars |
||||||
|
# Storage slot is not declared as a constant, but rather is hardcoded in the assembly, |
||||||
|
# so create a new StateVariable to represent it. |
||||||
|
delegate = create_state_variable_from_slot(dest.value) |
||||||
|
break |
||||||
|
return delegate |
||||||
|
|
||||||
|
|
||||||
|
def extract_delegate_from_asm(contract: Contract, node: Node) -> Optional[Variable]: |
||||||
|
""" |
||||||
|
Finds a Variable with a name matching the argument passed into a delegatecall, when all we have is an Assembly node |
||||||
|
with a block of code as one long string. Usually only the case for solc versions < 0.6.0. |
||||||
|
Can return a newly created StateVariable if an `sload` from a hardcoded storage slot is found in assembly. |
||||||
|
Should typically be called by find_delegate_in_fallback(proxy). |
||||||
|
Args: |
||||||
|
contract: The parent Contract. |
||||||
|
node: The Assembly Node (i.e., node.type == NodeType.ASSEMBLY) |
||||||
|
|
||||||
|
Returns: |
||||||
|
(`Variable`) | None : The variable being passed as the destination argument in a delegatecall in the fallback. |
||||||
|
""" |
||||||
|
asm_split = str(node.inline_asm).split("\n") |
||||||
|
asm = next(line for line in asm_split if "delegatecall" in line) |
||||||
|
params = asm.split("call(")[1].split(", ") |
||||||
|
dest = params[1] |
||||||
|
if dest.endswith(")") and not dest.startswith("sload("): |
||||||
|
dest = params[2] |
||||||
|
if dest.startswith("sload("): |
||||||
|
dest = dest.replace(")", "(").split("(")[1] |
||||||
|
if dest.startswith("0x"): |
||||||
|
return create_state_variable_from_slot(dest) |
||||||
|
if dest.isnumeric(): |
||||||
|
slot_idx = int(dest) |
||||||
|
return next( |
||||||
|
( |
||||||
|
v |
||||||
|
for v in contract.state_variables_ordered |
||||||
|
if SlitherReadStorage.get_variable_info(contract, v)[0] == slot_idx |
||||||
|
), |
||||||
|
None, |
||||||
|
) |
||||||
|
for v in node.function.variables_read_or_written: |
||||||
|
if v.name == dest: |
||||||
|
if isinstance(v, LocalVariable) and v.expression is not None: |
||||||
|
e = v.expression |
||||||
|
if isinstance(e, Identifier) and isinstance(e.value, StateVariable): |
||||||
|
v = e.value |
||||||
|
# Fall through, return constant storage slot |
||||||
|
if isinstance(v, StateVariable) and v.is_constant: |
||||||
|
return v |
||||||
|
if "_fallback_asm" in dest or "_slot" in dest: |
||||||
|
dest = dest.split("_")[0] |
||||||
|
return find_delegate_from_name(contract, dest, node.function) |
||||||
|
|
||||||
|
|
||||||
|
def find_delegate_from_name( |
||||||
|
contract: Contract, dest: str, parent_func: Function |
||||||
|
) -> Optional[Variable]: |
||||||
|
""" |
||||||
|
Searches for a variable with a given name, starting with StateVariables declared in the contract, followed by |
||||||
|
LocalVariables in the parent function, either declared in the function body or as parameters in the signature. |
||||||
|
Can return a newly created StateVariable if an `sload` from a hardcoded storage slot is found in assembly. |
||||||
|
Args: |
||||||
|
contract: The Contract object to search. |
||||||
|
dest: The variable name to search for. |
||||||
|
parent_func: The Function object to search. |
||||||
|
|
||||||
|
Returns: |
||||||
|
(`Variable`) | None : The variable with the matching name, if found |
||||||
|
""" |
||||||
|
for sv in contract.state_variables: |
||||||
|
if sv.name == dest: |
||||||
|
return sv |
||||||
|
for lv in parent_func.local_variables: |
||||||
|
if lv.name == dest: |
||||||
|
return lv |
||||||
|
for pv in parent_func.parameters + parent_func.returns: |
||||||
|
if pv.name == dest: |
||||||
|
return pv |
||||||
|
if parent_func.contains_assembly: |
||||||
|
for node in parent_func.all_nodes(): |
||||||
|
if node.type == NodeType.ASSEMBLY and isinstance(node.inline_asm, str): |
||||||
|
asm = next( |
||||||
|
( |
||||||
|
s |
||||||
|
for s in node.inline_asm.split("\n") |
||||||
|
if f"{dest}:=sload(" in s.replace(" ", "") |
||||||
|
), |
||||||
|
None, |
||||||
|
) |
||||||
|
if asm: |
||||||
|
slot = asm.split("sload(")[1].split(")")[0] |
||||||
|
if slot.startswith("0x"): |
||||||
|
return create_state_variable_from_slot(slot, name=dest) |
||||||
|
try: |
||||||
|
slot_idx = int(slot) |
||||||
|
return next( |
||||||
|
( |
||||||
|
v |
||||||
|
for v in contract.state_variables_ordered |
||||||
|
if SlitherReadStorage.get_variable_info(contract, v)[0] == slot_idx |
||||||
|
), |
||||||
|
None, |
||||||
|
) |
||||||
|
except TypeError: |
||||||
|
continue |
||||||
|
return None |
||||||
|
|
||||||
|
|
||||||
|
def create_state_variable_from_slot(slot: str, name: str = None) -> Optional[StateVariable]: |
||||||
|
""" |
||||||
|
Creates a new StateVariable object to wrap a hardcoded storage slot found in assembly. |
||||||
|
Args: |
||||||
|
slot: The storage slot hex string. |
||||||
|
name: Optional name for the variable. The slot string is used if name is not provided. |
||||||
|
|
||||||
|
Returns: |
||||||
|
A newly created constant StateVariable of type bytes32, with the slot as the variable's expression and name, |
||||||
|
if slot matches the length and prefix of a bytes32. Otherwise, returns None. |
||||||
|
""" |
||||||
|
if len(slot) == 66 and slot.startswith("0x"): # 32 bytes = 64 chars + "0x" = 66 chars |
||||||
|
# Storage slot is not declared as a constant, but rather is hardcoded in the assembly, |
||||||
|
# so create a new StateVariable to represent it. |
||||||
|
v = StateVariable() |
||||||
|
v.is_constant = True |
||||||
|
v.expression = Literal(slot, ElementaryType("bytes32")) |
||||||
|
if name is not None: |
||||||
|
v.name = name |
||||||
|
else: |
||||||
|
v.name = slot |
||||||
|
v.type = ElementaryType("bytes32") |
||||||
|
return v |
||||||
|
# This should probably also handle hashed strings, but for now return None |
||||||
|
return None |
@ -0,0 +1,29 @@ |
|||||||
|
pragma solidity ^0.6.12; |
||||||
|
|
||||||
|
contract FakeFallback { |
||||||
|
mapping(address => uint) public contributions; |
||||||
|
address payable public owner; |
||||||
|
|
||||||
|
constructor() public { |
||||||
|
owner = payable(msg.sender); |
||||||
|
contributions[msg.sender] = 1000 * (1 ether); |
||||||
|
} |
||||||
|
|
||||||
|
function fallback() public payable { |
||||||
|
contributions[msg.sender] += msg.value; |
||||||
|
} |
||||||
|
|
||||||
|
function receive() public payable { |
||||||
|
contributions[msg.sender] += msg.value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
contract Fallback is FakeFallback { |
||||||
|
receive() external payable { |
||||||
|
contributions[msg.sender] += msg.value; |
||||||
|
} |
||||||
|
|
||||||
|
fallback() external payable { |
||||||
|
contributions[msg.sender] += msg.value; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
from pathlib import Path |
||||||
|
from solc_select import solc_select |
||||||
|
|
||||||
|
from slither import Slither |
||||||
|
from slither.core.declarations.function import FunctionType |
||||||
|
|
||||||
|
TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" |
||||||
|
|
||||||
|
|
||||||
|
def test_fallback_receive(): |
||||||
|
solc_select.switch_global_version("0.6.12", always_install=True) |
||||||
|
file = Path(TEST_DATA_DIR, "fallback.sol").as_posix() |
||||||
|
slither = Slither(file) |
||||||
|
fake_fallback = slither.get_contract_from_name("FakeFallback")[0] |
||||||
|
real_fallback = slither.get_contract_from_name("Fallback")[0] |
||||||
|
|
||||||
|
assert fake_fallback.fallback_function is None |
||||||
|
assert fake_fallback.receive_function is None |
||||||
|
assert real_fallback.fallback_function.function_type == FunctionType.FALLBACK |
||||||
|
assert real_fallback.receive_function.function_type == FunctionType.RECEIVE |
@ -0,0 +1,6 @@ |
|||||||
|
pragma solidity ^0.5.0; |
||||||
|
|
||||||
|
import "./src/EIP1822Proxy.sol"; |
||||||
|
import "./src/ZosProxy.sol"; |
||||||
|
import "./src/MasterCopyProxy.sol"; |
||||||
|
import "./src/SynthProxy.sol"; |
@ -0,0 +1,6 @@ |
|||||||
|
pragma solidity ^0.8.2; |
||||||
|
|
||||||
|
import "./src/ContractV1.sol"; |
||||||
|
import "./src/ContractV2.sol"; |
||||||
|
import "./src/InheritedStorageProxy.sol"; |
||||||
|
import "./src/ERC1967Proxy.sol"; |
@ -0,0 +1,244 @@ |
|||||||
|
// SPDX-License-Identifier: MIT |
||||||
|
// OpenZeppelin Contracts (last updated v4.8.0) (utils/Address.sol) |
||||||
|
|
||||||
|
pragma solidity ^0.8.1; |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Collection of functions related to the address type |
||||||
|
*/ |
||||||
|
library Address { |
||||||
|
/** |
||||||
|
* @dev Returns true if `account` is a contract. |
||||||
|
* |
||||||
|
* [IMPORTANT] |
||||||
|
* ==== |
||||||
|
* It is unsafe to assume that an address for which this function returns |
||||||
|
* false is an externally-owned account (EOA) and not a contract. |
||||||
|
* |
||||||
|
* Among others, `isContract` will return false for the following |
||||||
|
* types of addresses: |
||||||
|
* |
||||||
|
* - an externally-owned account |
||||||
|
* - a contract in construction |
||||||
|
* - an address where a contract will be created |
||||||
|
* - an address where a contract lived, but was destroyed |
||||||
|
* ==== |
||||||
|
* |
||||||
|
* [IMPORTANT] |
||||||
|
* ==== |
||||||
|
* You shouldn't rely on `isContract` to protect against flash loan attacks! |
||||||
|
* |
||||||
|
* Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets |
||||||
|
* like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract |
||||||
|
* constructor. |
||||||
|
* ==== |
||||||
|
*/ |
||||||
|
function isContract(address account) internal view returns (bool) { |
||||||
|
// This method relies on extcodesize/address.code.length, which returns 0 |
||||||
|
// for contracts in construction, since the code is only stored at the end |
||||||
|
// of the constructor execution. |
||||||
|
|
||||||
|
return account.code.length > 0; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Replacement for Solidity's `transfer`: sends `amount` wei to |
||||||
|
* `recipient`, forwarding all available gas and reverting on errors. |
||||||
|
* |
||||||
|
* https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost |
||||||
|
* of certain opcodes, possibly making contracts go over the 2300 gas limit |
||||||
|
* imposed by `transfer`, making them unable to receive funds via |
||||||
|
* `transfer`. {sendValue} removes this limitation. |
||||||
|
* |
||||||
|
* https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. |
||||||
|
* |
||||||
|
* IMPORTANT: because control is transferred to `recipient`, care must be |
||||||
|
* taken to not create reentrancy vulnerabilities. Consider using |
||||||
|
* {ReentrancyGuard} or the |
||||||
|
* https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. |
||||||
|
*/ |
||||||
|
function sendValue(address payable recipient, uint256 amount) internal { |
||||||
|
require(address(this).balance >= amount, "Address: insufficient balance"); |
||||||
|
|
||||||
|
(bool success, ) = recipient.call{value: amount}(""); |
||||||
|
require(success, "Address: unable to send value, recipient may have reverted"); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Performs a Solidity function call using a low level `call`. A |
||||||
|
* plain `call` is an unsafe replacement for a function call: use this |
||||||
|
* function instead. |
||||||
|
* |
||||||
|
* If `target` reverts with a revert reason, it is bubbled up by this |
||||||
|
* function (like regular Solidity function calls). |
||||||
|
* |
||||||
|
* Returns the raw returned data. To convert to the expected return value, |
||||||
|
* use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. |
||||||
|
* |
||||||
|
* Requirements: |
||||||
|
* |
||||||
|
* - `target` must be a contract. |
||||||
|
* - calling `target` with `data` must not revert. |
||||||
|
* |
||||||
|
* _Available since v3.1._ |
||||||
|
*/ |
||||||
|
function functionCall(address target, bytes memory data) internal returns (bytes memory) { |
||||||
|
return functionCallWithValue(target, data, 0, "Address: low-level call failed"); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with |
||||||
|
* `errorMessage` as a fallback revert reason when `target` reverts. |
||||||
|
* |
||||||
|
* _Available since v3.1._ |
||||||
|
*/ |
||||||
|
function functionCall( |
||||||
|
address target, |
||||||
|
bytes memory data, |
||||||
|
string memory errorMessage |
||||||
|
) internal returns (bytes memory) { |
||||||
|
return functionCallWithValue(target, data, 0, errorMessage); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], |
||||||
|
* but also transferring `value` wei to `target`. |
||||||
|
* |
||||||
|
* Requirements: |
||||||
|
* |
||||||
|
* - the calling contract must have an ETH balance of at least `value`. |
||||||
|
* - the called Solidity function must be `payable`. |
||||||
|
* |
||||||
|
* _Available since v3.1._ |
||||||
|
*/ |
||||||
|
function functionCallWithValue( |
||||||
|
address target, |
||||||
|
bytes memory data, |
||||||
|
uint256 value |
||||||
|
) internal returns (bytes memory) { |
||||||
|
return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but |
||||||
|
* with `errorMessage` as a fallback revert reason when `target` reverts. |
||||||
|
* |
||||||
|
* _Available since v3.1._ |
||||||
|
*/ |
||||||
|
function functionCallWithValue( |
||||||
|
address target, |
||||||
|
bytes memory data, |
||||||
|
uint256 value, |
||||||
|
string memory errorMessage |
||||||
|
) internal returns (bytes memory) { |
||||||
|
require(address(this).balance >= value, "Address: insufficient balance for call"); |
||||||
|
(bool success, bytes memory returndata) = target.call{value: value}(data); |
||||||
|
return verifyCallResultFromTarget(target, success, returndata, errorMessage); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], |
||||||
|
* but performing a static call. |
||||||
|
* |
||||||
|
* _Available since v3.3._ |
||||||
|
*/ |
||||||
|
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { |
||||||
|
return functionStaticCall(target, data, "Address: low-level static call failed"); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], |
||||||
|
* but performing a static call. |
||||||
|
* |
||||||
|
* _Available since v3.3._ |
||||||
|
*/ |
||||||
|
function functionStaticCall( |
||||||
|
address target, |
||||||
|
bytes memory data, |
||||||
|
string memory errorMessage |
||||||
|
) internal view returns (bytes memory) { |
||||||
|
(bool success, bytes memory returndata) = target.staticcall(data); |
||||||
|
return verifyCallResultFromTarget(target, success, returndata, errorMessage); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], |
||||||
|
* but performing a delegate call. |
||||||
|
* |
||||||
|
* _Available since v3.4._ |
||||||
|
*/ |
||||||
|
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { |
||||||
|
return functionDelegateCall(target, data, "Address: low-level delegate call failed"); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], |
||||||
|
* but performing a delegate call. |
||||||
|
* |
||||||
|
* _Available since v3.4._ |
||||||
|
*/ |
||||||
|
function functionDelegateCall( |
||||||
|
address target, |
||||||
|
bytes memory data, |
||||||
|
string memory errorMessage |
||||||
|
) internal returns (bytes memory) { |
||||||
|
(bool success, bytes memory returndata) = target.delegatecall(data); |
||||||
|
return verifyCallResultFromTarget(target, success, returndata, errorMessage); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling |
||||||
|
* the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract. |
||||||
|
* |
||||||
|
* _Available since v4.8._ |
||||||
|
*/ |
||||||
|
function verifyCallResultFromTarget( |
||||||
|
address target, |
||||||
|
bool success, |
||||||
|
bytes memory returndata, |
||||||
|
string memory errorMessage |
||||||
|
) internal view returns (bytes memory) { |
||||||
|
if (success) { |
||||||
|
if (returndata.length == 0) { |
||||||
|
// only check isContract if the call was successful and the return data is empty |
||||||
|
// otherwise we already know that it was a contract |
||||||
|
require(isContract(target), "Address: call to non-contract"); |
||||||
|
} |
||||||
|
return returndata; |
||||||
|
} else { |
||||||
|
_revert(returndata, errorMessage); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the |
||||||
|
* revert reason or using the provided one. |
||||||
|
* |
||||||
|
* _Available since v4.3._ |
||||||
|
*/ |
||||||
|
function verifyCallResult( |
||||||
|
bool success, |
||||||
|
bytes memory returndata, |
||||||
|
string memory errorMessage |
||||||
|
) internal pure returns (bytes memory) { |
||||||
|
if (success) { |
||||||
|
return returndata; |
||||||
|
} else { |
||||||
|
_revert(returndata, errorMessage); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function _revert(bytes memory returndata, string memory errorMessage) private pure { |
||||||
|
// Look for revert reason and bubble it up if present |
||||||
|
if (returndata.length > 0) { |
||||||
|
// The easiest way to bubble the revert reason is using memory via assembly |
||||||
|
/// @solidity memory-safe-assembly |
||||||
|
assembly { |
||||||
|
let returndata_size := mload(returndata) |
||||||
|
revert(add(32, returndata), returndata_size) |
||||||
|
} |
||||||
|
} else { |
||||||
|
revert(errorMessage); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
pragma solidity ^0.8.2; |
||||||
|
|
||||||
|
import "./ProxyStorage.sol"; |
||||||
|
|
||||||
|
contract ContractV1 is ProxyStorage { |
||||||
|
uint private stateA = 0; |
||||||
|
uint private stateB = 0; |
||||||
|
uint constant CONST = 32; |
||||||
|
bool bug = false; |
||||||
|
|
||||||
|
function f(uint x) public { |
||||||
|
if (msg.sender == admin) { |
||||||
|
stateA = x; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function g(uint y) public { |
||||||
|
if (checkA()) { |
||||||
|
stateB = y - 10; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function h() public { |
||||||
|
if (checkB()) { |
||||||
|
bug = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function checkA() internal returns (bool) { |
||||||
|
return stateA % CONST == 1; |
||||||
|
} |
||||||
|
|
||||||
|
function checkB() internal returns (bool) { |
||||||
|
return stateB == 62; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
pragma solidity ^0.8.2; |
||||||
|
|
||||||
|
import "./ProxyStorage.sol"; |
||||||
|
|
||||||
|
contract ContractV2 is ProxyStorage { |
||||||
|
uint private stateA = 0; |
||||||
|
uint private stateB = 0; |
||||||
|
uint constant CONST = 32; |
||||||
|
bool bug = false; |
||||||
|
uint private stateC = 0; |
||||||
|
|
||||||
|
function f(uint x) public { |
||||||
|
if (msg.sender == admin) { |
||||||
|
stateA = x; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function g(uint y) public { |
||||||
|
if (checkA()) { |
||||||
|
stateB = y - 10; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function h() public { |
||||||
|
if (checkB()) { |
||||||
|
bug = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function i() public { |
||||||
|
stateC = stateC + 1; |
||||||
|
} |
||||||
|
|
||||||
|
function checkA() internal returns (bool) { |
||||||
|
return stateA % CONST == 1; |
||||||
|
} |
||||||
|
|
||||||
|
function checkB() internal returns (bool) { |
||||||
|
return stateB == 32; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,47 @@ |
|||||||
|
pragma solidity ^0.5.0; |
||||||
|
|
||||||
|
contract EIP1822Proxy { |
||||||
|
// Code position in storage is keccak256("PROXIABLE") = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7" |
||||||
|
constructor(bytes memory constructData, address contractLogic) public { |
||||||
|
// save the code address |
||||||
|
assembly { // solium-disable-line |
||||||
|
sstore(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7, contractLogic) |
||||||
|
} |
||||||
|
(bool success, bytes memory _ ) = contractLogic.delegatecall(constructData); // solium-disable-line |
||||||
|
require(success, "Construction failed"); |
||||||
|
} |
||||||
|
|
||||||
|
function() external payable { |
||||||
|
assembly { // solium-disable-line |
||||||
|
let contractLogic := sload(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7) |
||||||
|
calldatacopy(0x0, 0x0, calldatasize) |
||||||
|
let success := delegatecall(sub(gas, 10000), contractLogic, 0x0, calldatasize, 0, 0) |
||||||
|
let retSz := returndatasize |
||||||
|
returndatacopy(0, 0, retSz) |
||||||
|
switch success |
||||||
|
case 0 { |
||||||
|
revert(0, retSz) |
||||||
|
} |
||||||
|
default { |
||||||
|
return(0, retSz) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
contract EIP1822Proxiable { |
||||||
|
// Code position in storage is keccak256("PROXIABLE") = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7" |
||||||
|
|
||||||
|
function updateCodeAddress(address newAddress) internal { |
||||||
|
require( |
||||||
|
bytes32(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7) == EIP1822Proxiable(newAddress).proxiableUUID(), |
||||||
|
"Not compatible" |
||||||
|
); |
||||||
|
assembly { // solium-disable-line |
||||||
|
sstore(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7, newAddress) |
||||||
|
} |
||||||
|
} |
||||||
|
function proxiableUUID() public pure returns (bytes32) { |
||||||
|
return 0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
pragma solidity ^0.8.0; |
||||||
|
|
||||||
|
import "./Proxy.sol"; |
||||||
|
import "./ERC1967Upgrade.sol"; |
||||||
|
|
||||||
|
contract ERC1967Proxy is Proxy, ERC1967Upgrade { |
||||||
|
|
||||||
|
constructor(address _logic, bytes memory _data) payable { |
||||||
|
_upgradeToAndCall(_logic, _data, false); |
||||||
|
} |
||||||
|
|
||||||
|
function _implementation() internal view virtual override returns (address impl) { |
||||||
|
return ERC1967Upgrade._getImplementation(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,105 @@ |
|||||||
|
pragma solidity ^0.8.2; |
||||||
|
|
||||||
|
import "./Address.sol"; |
||||||
|
import "./StorageSlot.sol"; |
||||||
|
|
||||||
|
interface IBeacon { |
||||||
|
function implementation() external view returns (address); |
||||||
|
} |
||||||
|
|
||||||
|
interface IERC1822Proxiable { |
||||||
|
function proxiableUUID() external view returns (bytes32); |
||||||
|
} |
||||||
|
|
||||||
|
abstract contract ERC1967Upgrade { |
||||||
|
|
||||||
|
bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143; |
||||||
|
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; |
||||||
|
bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; |
||||||
|
bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; |
||||||
|
|
||||||
|
event Upgraded(address indexed implementation); |
||||||
|
event AdminChanged(address previousAdmin, address newAdmin); |
||||||
|
event BeaconUpgraded(address indexed beacon); |
||||||
|
|
||||||
|
function _getImplementation() internal view returns (address) { |
||||||
|
return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; |
||||||
|
} |
||||||
|
|
||||||
|
function _setImplementation(address newImplementation) private { |
||||||
|
require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); |
||||||
|
StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; |
||||||
|
} |
||||||
|
|
||||||
|
function _upgradeTo(address newImplementation) internal { |
||||||
|
_setImplementation(newImplementation); |
||||||
|
emit Upgraded(newImplementation); |
||||||
|
} |
||||||
|
|
||||||
|
function _upgradeToAndCall( |
||||||
|
address newImplementation, |
||||||
|
bytes memory data, |
||||||
|
bool forceCall |
||||||
|
) internal { |
||||||
|
_upgradeTo(newImplementation); |
||||||
|
if (data.length > 0 || forceCall) { |
||||||
|
Address.functionDelegateCall(newImplementation, data); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function _upgradeToAndCallUUPS( |
||||||
|
address newImplementation, |
||||||
|
bytes memory data, |
||||||
|
bool forceCall |
||||||
|
) internal { |
||||||
|
if (StorageSlot.getBooleanSlot(_ROLLBACK_SLOT).value) { |
||||||
|
_setImplementation(newImplementation); |
||||||
|
} else { |
||||||
|
try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) { |
||||||
|
require(slot == _IMPLEMENTATION_SLOT, "ERC1967Upgrade: unsupported proxiableUUID"); |
||||||
|
} catch { |
||||||
|
revert("ERC1967Upgrade: new implementation is not UUPS"); |
||||||
|
} |
||||||
|
_upgradeToAndCall(newImplementation, data, forceCall); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function _getAdmin() internal view returns (address) { |
||||||
|
return StorageSlot.getAddressSlot(_ADMIN_SLOT).value; |
||||||
|
} |
||||||
|
|
||||||
|
function _setAdmin(address newAdmin) private { |
||||||
|
require(newAdmin != address(0), "ERC1967: new admin is the zero address"); |
||||||
|
StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin; |
||||||
|
} |
||||||
|
|
||||||
|
function _changeAdmin(address newAdmin) internal { |
||||||
|
emit AdminChanged(_getAdmin(), newAdmin); |
||||||
|
_setAdmin(newAdmin); |
||||||
|
} |
||||||
|
|
||||||
|
function _getBeacon() internal view returns (address) { |
||||||
|
return StorageSlot.getAddressSlot(_BEACON_SLOT).value; |
||||||
|
} |
||||||
|
|
||||||
|
function _setBeacon(address newBeacon) private { |
||||||
|
require(Address.isContract(newBeacon), "ERC1967: new beacon is not a contract"); |
||||||
|
require( |
||||||
|
Address.isContract(IBeacon(newBeacon).implementation()), |
||||||
|
"ERC1967: beacon implementation is not a contract" |
||||||
|
); |
||||||
|
StorageSlot.getAddressSlot(_BEACON_SLOT).value = newBeacon; |
||||||
|
} |
||||||
|
|
||||||
|
function _upgradeBeaconToAndCall( |
||||||
|
address newBeacon, |
||||||
|
bytes memory data, |
||||||
|
bool forceCall |
||||||
|
) internal { |
||||||
|
_setBeacon(newBeacon); |
||||||
|
emit BeaconUpgraded(newBeacon); |
||||||
|
if (data.length > 0 || forceCall) { |
||||||
|
Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
pragma solidity ^0.8.0; |
||||||
|
|
||||||
|
import "./Proxy.sol"; |
||||||
|
import "./ProxyStorage.sol"; |
||||||
|
|
||||||
|
contract InheritedStorageProxy is Proxy, ProxyStorage { |
||||||
|
constructor(address _implementation) { |
||||||
|
admin = msg.sender; |
||||||
|
implementation = _implementation; |
||||||
|
} |
||||||
|
|
||||||
|
function getImplementation() external view returns (address) { |
||||||
|
return _implementation(); |
||||||
|
} |
||||||
|
|
||||||
|
function getAdmin() external view returns (address) { |
||||||
|
return _admin(); |
||||||
|
} |
||||||
|
|
||||||
|
function upgrade(address _newImplementation) external { |
||||||
|
require(msg.sender == admin, "Only admin can upgrade"); |
||||||
|
implementation = _newImplementation; |
||||||
|
} |
||||||
|
|
||||||
|
function setAdmin(address _newAdmin) external { |
||||||
|
require(msg.sender == admin, "Only current admin can change admin"); |
||||||
|
admin = _newAdmin; |
||||||
|
} |
||||||
|
|
||||||
|
function _implementation() internal view override returns (address) { |
||||||
|
return implementation; |
||||||
|
} |
||||||
|
|
||||||
|
function _admin() internal view returns (address) { |
||||||
|
return admin; |
||||||
|
} |
||||||
|
|
||||||
|
function _beforeFallback() internal override {} |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
pragma solidity ^0.5.0; |
||||||
|
|
||||||
|
contract MasterCopyProxy { |
||||||
|
address internal masterCopy; |
||||||
|
|
||||||
|
constructor(address _masterCopy) |
||||||
|
public |
||||||
|
{ |
||||||
|
require(_masterCopy != address(0), "Invalid master copy address provided"); |
||||||
|
masterCopy = _masterCopy; |
||||||
|
} |
||||||
|
|
||||||
|
/// @dev Fallback function forwards all transactions and returns all received return data. |
||||||
|
function () |
||||||
|
external |
||||||
|
payable |
||||||
|
{ |
||||||
|
// solium-disable-next-line security/no-inline-assembly |
||||||
|
assembly { |
||||||
|
calldatacopy(0, 0, calldatasize()) |
||||||
|
let success := delegatecall(gas, sload(0), 0, calldatasize(), 0, 0) |
||||||
|
returndatacopy(0, 0, returndatasize()) |
||||||
|
if eq(success, 0) { revert(0, returndatasize()) } |
||||||
|
return(0, returndatasize()) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
pragma solidity ^0.8.0; |
||||||
|
|
||||||
|
abstract contract Proxy { |
||||||
|
|
||||||
|
function _delegate(address implementation) internal virtual { |
||||||
|
assembly { |
||||||
|
calldatacopy(0, 0, calldatasize()) |
||||||
|
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) |
||||||
|
returndatacopy(0, 0, returndatasize()) |
||||||
|
switch result |
||||||
|
case 0 { |
||||||
|
revert(0, returndatasize()) |
||||||
|
} |
||||||
|
default { |
||||||
|
return(0, returndatasize()) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function _implementation() internal view virtual returns (address); |
||||||
|
|
||||||
|
function _fallback() internal virtual { |
||||||
|
_beforeFallback(); |
||||||
|
_delegate(_implementation()); |
||||||
|
} |
||||||
|
|
||||||
|
fallback() external payable virtual { |
||||||
|
_fallback(); |
||||||
|
} |
||||||
|
|
||||||
|
receive() external payable virtual { |
||||||
|
_fallback(); |
||||||
|
} |
||||||
|
|
||||||
|
function _beforeFallback() internal virtual {} |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
pragma solidity ^0.8.0; |
||||||
|
|
||||||
|
contract ProxyStorage { |
||||||
|
address internal admin; |
||||||
|
address internal implementation; |
||||||
|
} |
@ -0,0 +1,88 @@ |
|||||||
|
// SPDX-License-Identifier: MIT |
||||||
|
// OpenZeppelin Contracts (last updated v4.7.0) (utils/StorageSlot.sol) |
||||||
|
|
||||||
|
pragma solidity ^0.8.0; |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Library for reading and writing primitive types to specific storage slots. |
||||||
|
* |
||||||
|
* Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. |
||||||
|
* This library helps with reading and writing to such slots without the need for inline assembly. |
||||||
|
* |
||||||
|
* The functions in this library return Slot structs that contain a `value` member that can be used to read or write. |
||||||
|
* |
||||||
|
* Example usage to set ERC1967 implementation slot: |
||||||
|
* ``` |
||||||
|
* contract ERC1967 { |
||||||
|
* bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; |
||||||
|
* |
||||||
|
* function _getImplementation() internal view returns (address) { |
||||||
|
* return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; |
||||||
|
* } |
||||||
|
* |
||||||
|
* function _setImplementation(address newImplementation) internal { |
||||||
|
* require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); |
||||||
|
* StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; |
||||||
|
* } |
||||||
|
* } |
||||||
|
* ``` |
||||||
|
* |
||||||
|
* _Available since v4.1 for `address`, `bool`, `bytes32`, and `uint256`._ |
||||||
|
*/ |
||||||
|
library StorageSlot { |
||||||
|
struct AddressSlot { |
||||||
|
address value; |
||||||
|
} |
||||||
|
|
||||||
|
struct BooleanSlot { |
||||||
|
bool value; |
||||||
|
} |
||||||
|
|
||||||
|
struct Bytes32Slot { |
||||||
|
bytes32 value; |
||||||
|
} |
||||||
|
|
||||||
|
struct Uint256Slot { |
||||||
|
uint256 value; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Returns an `AddressSlot` with member `value` located at `slot`. |
||||||
|
*/ |
||||||
|
function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { |
||||||
|
/// @solidity memory-safe-assembly |
||||||
|
assembly { |
||||||
|
r.slot := slot |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Returns an `BooleanSlot` with member `value` located at `slot`. |
||||||
|
*/ |
||||||
|
function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) { |
||||||
|
/// @solidity memory-safe-assembly |
||||||
|
assembly { |
||||||
|
r.slot := slot |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Returns an `Bytes32Slot` with member `value` located at `slot`. |
||||||
|
*/ |
||||||
|
function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) { |
||||||
|
/// @solidity memory-safe-assembly |
||||||
|
assembly { |
||||||
|
r.slot := slot |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Returns an `Uint256Slot` with member `value` located at `slot`. |
||||||
|
*/ |
||||||
|
function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) { |
||||||
|
/// @solidity memory-safe-assembly |
||||||
|
assembly { |
||||||
|
r.slot := slot |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,58 @@ |
|||||||
|
pragma solidity ^0.5.0; |
||||||
|
|
||||||
|
contract Owned { |
||||||
|
address public owner; |
||||||
|
|
||||||
|
constructor(address _owner) public { |
||||||
|
require(_owner != address(0), "Owner address cannot be 0"); |
||||||
|
owner = _owner; |
||||||
|
} |
||||||
|
|
||||||
|
modifier onlyOwner { |
||||||
|
require(msg.sender == owner, "Only the contract owner may perform this action"); |
||||||
|
_; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
contract Proxyable is Owned { |
||||||
|
/* The proxy this contract exists behind. */ |
||||||
|
SynthProxy public proxy; |
||||||
|
|
||||||
|
constructor(address payable _proxy) internal { |
||||||
|
// This contract is abstract, and thus cannot be instantiated directly |
||||||
|
require(owner != address(0), "Owner must be set"); |
||||||
|
|
||||||
|
proxy = SynthProxy(_proxy); |
||||||
|
} |
||||||
|
|
||||||
|
function setProxy(address payable _proxy) external onlyOwner { |
||||||
|
proxy = SynthProxy(_proxy); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
contract SynthProxy is Owned { |
||||||
|
Proxyable public target; |
||||||
|
|
||||||
|
constructor(address _owner) public Owned(_owner) {} |
||||||
|
|
||||||
|
function setTarget(Proxyable _target) external onlyOwner { |
||||||
|
target = _target; |
||||||
|
} |
||||||
|
|
||||||
|
// solhint-disable no-complex-fallback |
||||||
|
function() external payable { |
||||||
|
assembly { |
||||||
|
calldatacopy(0, 0, calldatasize) |
||||||
|
|
||||||
|
/* We must explicitly forward ether to the underlying contract as well. */ |
||||||
|
let result := delegatecall(gas, sload(target_slot), 0, calldatasize, 0, 0) |
||||||
|
returndatacopy(0, 0, returndatasize) |
||||||
|
|
||||||
|
if iszero(result) { |
||||||
|
revert(0, returndatasize) |
||||||
|
} |
||||||
|
return(0, returndatasize) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,67 @@ |
|||||||
|
pragma solidity ^0.5.0; |
||||||
|
|
||||||
|
contract ZosProxy { |
||||||
|
function () payable external { |
||||||
|
_fallback(); |
||||||
|
} |
||||||
|
|
||||||
|
function _implementation() internal view returns (address); |
||||||
|
|
||||||
|
function _delegate(address implementation) internal { |
||||||
|
assembly { |
||||||
|
calldatacopy(0, 0, calldatasize) |
||||||
|
let result := delegatecall(gas, implementation, 0, calldatasize, 0, 0) |
||||||
|
returndatacopy(0, 0, returndatasize) |
||||||
|
switch result |
||||||
|
case 0 { revert(0, returndatasize) } |
||||||
|
default { return(0, returndatasize) } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function _willFallback() internal { |
||||||
|
} |
||||||
|
|
||||||
|
function _fallback() internal { |
||||||
|
_willFallback(); |
||||||
|
_delegate(_implementation()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
library AddressUtils { |
||||||
|
function isContract(address addr) internal view returns (bool) { |
||||||
|
uint256 size; |
||||||
|
assembly { size := extcodesize(addr) } |
||||||
|
return size > 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
contract UpgradeabilityProxy is ZosProxy { |
||||||
|
event Upgraded(address indexed implementation); |
||||||
|
|
||||||
|
bytes32 private constant IMPLEMENTATION_SLOT = 0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3; |
||||||
|
|
||||||
|
constructor(address _implementation) public payable { |
||||||
|
assert(IMPLEMENTATION_SLOT == keccak256("org.zeppelinos.proxy.implementation")); |
||||||
|
_setImplementation(_implementation); |
||||||
|
} |
||||||
|
|
||||||
|
function _implementation() internal view returns (address impl) { |
||||||
|
bytes32 slot = IMPLEMENTATION_SLOT; |
||||||
|
assembly { |
||||||
|
impl := sload(slot) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function _upgradeTo(address newImplementation) internal { |
||||||
|
_setImplementation(newImplementation); |
||||||
|
emit Upgraded(newImplementation); |
||||||
|
} |
||||||
|
|
||||||
|
function _setImplementation(address newImplementation) private { |
||||||
|
require(AddressUtils.isContract(newImplementation), "Cannot set a proxy implementation to a non-contract address"); |
||||||
|
bytes32 slot = IMPLEMENTATION_SLOT; |
||||||
|
assembly { |
||||||
|
sstore(slot, newImplementation) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,85 @@ |
|||||||
|
import os |
||||||
|
from pathlib import Path |
||||||
|
|
||||||
|
from solc_select import solc_select |
||||||
|
|
||||||
|
from slither import Slither |
||||||
|
from slither.core.expressions import Literal |
||||||
|
from slither.utils.upgradeability import ( |
||||||
|
compare, |
||||||
|
get_proxy_implementation_var, |
||||||
|
get_proxy_implementation_slot, |
||||||
|
) |
||||||
|
|
||||||
|
SLITHER_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
||||||
|
TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" / "upgradeability_util" |
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-locals |
||||||
|
def test_upgrades_compare() -> None: |
||||||
|
solc_select.switch_global_version("0.8.2", always_install=True) |
||||||
|
|
||||||
|
sl = Slither(os.path.join(TEST_DATA_DIR, "TestUpgrades-0.8.2.sol")) |
||||||
|
v1 = sl.get_contract_from_name("ContractV1")[0] |
||||||
|
v2 = sl.get_contract_from_name("ContractV2")[0] |
||||||
|
missing_vars, new_vars, tainted_vars, new_funcs, modified_funcs, tainted_funcs = compare(v1, v2) |
||||||
|
assert len(missing_vars) == 0 |
||||||
|
assert new_vars == [v2.get_state_variable_from_name("stateC")] |
||||||
|
assert tainted_vars == [ |
||||||
|
v2.get_state_variable_from_name("stateB"), |
||||||
|
v2.get_state_variable_from_name("bug"), |
||||||
|
] |
||||||
|
assert new_funcs == [v2.get_function_from_signature("i()")] |
||||||
|
assert modified_funcs == [v2.get_function_from_signature("checkB()")] |
||||||
|
assert tainted_funcs == [ |
||||||
|
v2.get_function_from_signature("g(uint256)"), |
||||||
|
v2.get_function_from_signature("h()"), |
||||||
|
] |
||||||
|
|
||||||
|
|
||||||
|
def test_upgrades_implementation_var() -> None: |
||||||
|
solc_select.switch_global_version("0.8.2", always_install=True) |
||||||
|
sl = Slither(os.path.join(TEST_DATA_DIR, "TestUpgrades-0.8.2.sol")) |
||||||
|
|
||||||
|
erc_1967_proxy = sl.get_contract_from_name("ERC1967Proxy")[0] |
||||||
|
storage_proxy = sl.get_contract_from_name("InheritedStorageProxy")[0] |
||||||
|
|
||||||
|
target = get_proxy_implementation_var(erc_1967_proxy) |
||||||
|
slot = get_proxy_implementation_slot(erc_1967_proxy) |
||||||
|
assert target == erc_1967_proxy.get_state_variable_from_name("_IMPLEMENTATION_SLOT") |
||||||
|
assert slot.slot == 0x360894A13BA1A3210667C828492DB98DCA3E2076CC3735A920A3CA505D382BBC |
||||||
|
target = get_proxy_implementation_var(storage_proxy) |
||||||
|
slot = get_proxy_implementation_slot(storage_proxy) |
||||||
|
assert target == storage_proxy.get_state_variable_from_name("implementation") |
||||||
|
assert slot.slot == 1 |
||||||
|
|
||||||
|
solc_select.switch_global_version("0.5.0", always_install=True) |
||||||
|
sl = Slither(os.path.join(TEST_DATA_DIR, "TestUpgrades-0.5.0.sol")) |
||||||
|
|
||||||
|
eip_1822_proxy = sl.get_contract_from_name("EIP1822Proxy")[0] |
||||||
|
# zos_proxy = sl.get_contract_from_name("ZosProxy")[0] |
||||||
|
master_copy_proxy = sl.get_contract_from_name("MasterCopyProxy")[0] |
||||||
|
synth_proxy = sl.get_contract_from_name("SynthProxy")[0] |
||||||
|
|
||||||
|
target = get_proxy_implementation_var(eip_1822_proxy) |
||||||
|
slot = get_proxy_implementation_slot(eip_1822_proxy) |
||||||
|
assert target not in eip_1822_proxy.state_variables_ordered |
||||||
|
assert target.name == "contractLogic" and isinstance(target.expression, Literal) |
||||||
|
assert ( |
||||||
|
target.expression.value |
||||||
|
== "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7" |
||||||
|
) |
||||||
|
assert slot.slot == 0xC5F16F0FCC639FA48A6947836D9850F504798523BF8C9A3A87D5876CF622BCF7 |
||||||
|
# # The util fails with this proxy due to how Slither parses assembly w/ Solidity versions < 0.6.0 (see issue #1775) |
||||||
|
# target = get_proxy_implementation_var(zos_proxy) |
||||||
|
# slot = get_proxy_implementation_slot(zos_proxy) |
||||||
|
# assert target == zos_proxy.get_state_variable_from_name("IMPLEMENTATION_SLOT") |
||||||
|
# assert slot.slot == 0x7050C9E0F4CA769C69BD3A8EF740BC37934F8E2C036E5A723FD8EE048ED3F8C3 |
||||||
|
target = get_proxy_implementation_var(master_copy_proxy) |
||||||
|
slot = get_proxy_implementation_slot(master_copy_proxy) |
||||||
|
assert target == master_copy_proxy.get_state_variable_from_name("masterCopy") |
||||||
|
assert slot.slot == 0 |
||||||
|
target = get_proxy_implementation_var(synth_proxy) |
||||||
|
slot = get_proxy_implementation_slot(synth_proxy) |
||||||
|
assert target == synth_proxy.get_state_variable_from_name("target") |
||||||
|
assert slot.slot == 1 |
Loading…
Reference in new issue