def parse_yul_function_call(root: YulScope, node: YulNode, ast: Dict) -> Optional[Expression]: args = [parse_yul(root, node, arg) for arg in ast["arguments"]] ident = parse_yul(root, node, ast["functionName"]) if not isinstance(ident, Identifier): raise FortressException( "expected identifier from parsing function name") if isinstance(ident.value, YulBuiltin): name = ident.value.name if name in binary_ops: if name in ["shl", "shr", "sar"]: # lmao ok return BinaryOperation(args[1], args[0], binary_ops[name]) return BinaryOperation(args[0], args[1], binary_ops[name]) if name in unary_ops: return UnaryOperation(args[0], unary_ops[name]) ident = Identifier( SolidityFunction(format_function_descriptor(ident.value.name))) if isinstance(ident.value, Function): return CallExpression(ident, args, vars_to_typestr(ident.value.returns)) if isinstance(ident.value, SolidityFunction): return CallExpression(ident, args, vars_to_typestr(ident.value.return_type)) raise FortressException( f"unexpected function call target type {str(type(ident.value))}")
def state_variable(self): if self._name.endswith("_slot"): return self._name[:-5] if self._name.endswith("_offset"): return self._name[:-7] to_log = f"Incorrect YUL parsing. {self} is not a solidity variable that can be seen as a state variable" raise FortressException(to_log)
def storage_size(self) -> Tuple[int, bool]: from fortress.core.declarations.structure import Structure from fortress.core.declarations.enum import Enum from fortress.core.declarations.contract import Contract if isinstance(self._type, Contract): return 20, False if isinstance(self._type, Enum): return int(math.ceil(math.log2(len(self._type.values)) / 8)), False if isinstance(self._type, Structure): # todo there's some duplicate logic here and fortress_core, can we refactor this? slot = 0 offset = 0 for elem in self._type.elems_ordered: size, new_slot = elem.type.storage_size if new_slot: if offset > 0: slot += 1 offset = 0 elif size + offset > 32: slot += 1 offset = 0 if new_slot: slot += math.ceil(size / 32) else: offset += size if offset > 0: slot += 1 return slot * 32, True to_log = f"{self} does not have storage size" raise FortressException(to_log)
def analyze_contracts(self): # pylint: disable=too-many-statements,too-many-branches if not self._parsed: raise FortressException( "Parse the contract before running analyses") self._convert_to_slithir() compute_dependency(self._core) self._core.compute_storage_layout() self._analyzed = True
def new_node(self, node_type: NodeType, src: Union[str, Dict]) -> YulNode: if self._parent_func: node = self._parent_func.new_node(node_type, src) else: raise FortressException( "standalone yul objects are not supported yet") yul_node = YulNode(node, self) self._nodes.append(yul_node) return yul_node
def new_node(self, node_type, src) -> YulNode: if self._function: node = self._function.new_node(node_type, src) else: raise FortressException( "standalone yul objects are not supported yet") yul_node = YulNode(node, self) self._nodes.append(yul_node) return yul_node
def parse_yul_identifier(root: YulScope, _node: YulNode, ast: Dict) -> Optional[Expression]: name = ast["name"] if name in builtins: return Identifier(YulBuiltin(name)) # check function-scoped variables if root.parent_func: variable = root.parent_func.get_local_variable_from_name(name) if variable: return Identifier(variable) variable = root.parent_func.contract.get_state_variable_from_name(name) if variable: return Identifier(variable) # check yul-scoped variable variable = root.get_yul_local_variable_from_name(name) if variable: return Identifier(variable.underlying) # check yul-scoped function func = root.get_yul_local_function_from_name(name) if func: return Identifier(func.underlying) # check for magic suffixes if name.endswith("_slot"): potential_name = name[:-5] var = root.function.contract.get_state_variable_from_name( potential_name) if var: return Identifier(var) var = root.function.get_local_variable_from_name(potential_name) if var and var.is_storage: return Identifier(var) if name.endswith("_offset"): potential_name = name[:-7] var = root.function.contract.get_state_variable_from_name( potential_name) if var: return Identifier(var) raise FortressException(f"unresolved reference to identifier {name}")
def parse_contracts(self): # pylint: disable=too-many-statements,too-many-branches if not self._underlying_contract_to_parser: logger.info( f"No contract were found in {self._core.filename}, check the correct compilation" ) if self._parsed: raise Exception("Contract analysis can be run only once!") # First we save all the contracts in a dict # the key is the contractid for contract in self._underlying_contract_to_parser: if (contract.name.startswith("FortressInternalTopLevelContract") and not contract.is_top_level): raise FortressException( """Your codebase has a contract named 'FortressInternalTopLevelContract'. Please rename it, this name is reserved for Fortress's internals""") if contract.name in self._core.contracts_as_dict: if contract.id != self._core.contracts_as_dict[ contract.name].id: self._core.contract_name_collisions[contract.name].append( contract.source_mapping_str) self._core.contract_name_collisions[contract.name].append( self._core.contracts_as_dict[ contract.name].source_mapping_str) else: self._contracts_by_id[contract.id] = contract self._core.contracts_as_dict[contract.name] = contract # Update of the inheritance for contract_parser in self._underlying_contract_to_parser.values(): # remove the first elem in linearizedBaseContracts as it is the contract itself ancestors = [] fathers = [] father_constructors = [] # try: # Resolve linearized base contracts. missing_inheritance = False for i in contract_parser.linearized_base_contracts[1:]: if i in contract_parser.remapping: ancestors.append( self._core.get_contract_from_name( contract_parser.remapping[i])) elif i in self._contracts_by_id: ancestors.append(self._contracts_by_id[i]) else: missing_inheritance = True # Resolve immediate base contracts for i in contract_parser.baseContracts: if i in contract_parser.remapping: fathers.append( self._core.get_contract_from_name( contract_parser.remapping[i])) elif i in self._contracts_by_id: fathers.append(self._contracts_by_id[i]) else: missing_inheritance = True # Resolve immediate base constructor calls for i in contract_parser.baseConstructorContractsCalled: if i in contract_parser.remapping: father_constructors.append( self._core.get_contract_from_name( contract_parser.remapping[i])) elif i in self._contracts_by_id: father_constructors.append(self._contracts_by_id[i]) else: missing_inheritance = True contract_parser.underlying_contract.set_inheritance( ancestors, fathers, father_constructors) if missing_inheritance: self._core.contracts_with_missing_inheritance.add( contract_parser.underlying_contract) contract_parser.log_incorrect_parsing( f"Missing inheritance {contract_parser}") contract_parser.set_is_analyzed(True) contract_parser.delete_content() contracts_to_be_analyzed = list( self._underlying_contract_to_parser.values()) # Any contract can refer another contract enum without need for inheritance self._analyze_all_enums(contracts_to_be_analyzed) # pylint: disable=expression-not-assigned [ c.set_is_analyzed(False) for c in self._underlying_contract_to_parser.values() ] libraries = [ c for c in contracts_to_be_analyzed if c.underlying_contract.contract_kind == "library" ] contracts_to_be_analyzed = [ c for c in contracts_to_be_analyzed if c.underlying_contract.contract_kind != "library" ] # We first parse the struct/variables/functions/contract self._analyze_first_part(contracts_to_be_analyzed, libraries) # pylint: disable=expression-not-assigned [ c.set_is_analyzed(False) for c in self._underlying_contract_to_parser.values() ] # We analyze the struct and parse and analyze the events # A contract can refer in the variables a struct or a event from any contract # (without inheritance link) self._analyze_second_part(contracts_to_be_analyzed, libraries) [ c.set_is_analyzed(False) for c in self._underlying_contract_to_parser.values() ] # Then we analyse state variables, functions and modifiers self._analyze_third_part(contracts_to_be_analyzed, libraries) self._parsed = True
def _get_source_code(self, contract: Contract): # pylint: disable=too-many-branches,too-many-statements """ Save the source code of the contract in self._source_codes Patch the source code :param contract: :return: """ src_mapping = contract.source_mapping content = self._fortress.source_code[ src_mapping["filename_absolute"]].encode("utf8") start = src_mapping["start"] end = src_mapping["start"] + src_mapping["length"] to_patch = [] # interface must use external if self._external_to_public and contract.contract_kind != "interface": for f in contract.functions_declared: # fallback must be external if f.is_fallback or f.is_constructor_variables: continue if f.visibility == "external": attributes_start = ( f.parameters_src.source_mapping["start"] + f.parameters_src.source_mapping["length"]) attributes_end = f.returns_src.source_mapping["start"] attributes = content[attributes_start:attributes_end] regex = re.search( r"((\sexternal)\s+)|(\sexternal)$|(\)external)$", attributes) if regex: to_patch.append( Patch( attributes_start + regex.span()[0] + 1, "public_to_external", )) else: raise FortressException( f"External keyword not found {f.name} {attributes}" ) for var in f.parameters: if var.location == "calldata": calldata_start = var.source_mapping["start"] calldata_end = calldata_start + var.source_mapping[ "length"] calldata_idx = content[ calldata_start:calldata_end].find(" calldata ") to_patch.append( Patch( calldata_start + calldata_idx + 1, "calldata_to_memory", )) if self._private_to_internal: for variable in contract.state_variables_declared: if variable.visibility == "private": print(variable.source_mapping) attributes_start = variable.source_mapping["start"] attributes_end = attributes_start + variable.source_mapping[ "length"] attributes = content[attributes_start:attributes_end] print(attributes) regex = re.search(r" private ", attributes) if regex: to_patch.append( Patch( attributes_start + regex.span()[0] + 1, "private_to_internal", )) else: raise FortressException( f"private keyword not found {variable.name} {attributes}" ) if self._remove_assert: for function in contract.functions_and_modifiers_declared: for node in function.nodes: for ir in node.irs: if isinstance( ir, SolidityCall ) and ir.function == SolidityFunction("assert(bool)"): to_patch.append( Patch(node.source_mapping["start"], "line_removal")) logger.info( f"Code commented: {node.expression} ({node.source_mapping_str})" ) to_patch.sort(key=lambda x: x.index, reverse=True) content = content[start:end] for patch in to_patch: patch_type = patch.patch_type index = patch.index index = index - start if patch_type == "public_to_external": content = content[:index] + "public" + content[ index + len("external"):] if patch_type == "private_to_internal": content = content[:index] + "internal" + content[ index + len("private"):] elif patch_type == "calldata_to_memory": content = content[:index] + "memory" + content[ index + len("calldata"):] else: assert patch_type == "line_removal" content = content[:index] + " // " + content[index:] self._source_codes[contract] = content.decode("utf8")
def max(self) -> int: if self.name in MaxValues: return MaxValues[self.name] raise FortressException(f"{self.name} does not have a max value")
def min(self) -> int: if self.name in MinValues: return MinValues[self.name] raise FortressException(f"{self.name} does not have a min value")
def parse_yul_unsupported(_root: YulScope, _node: YulNode, ast: Dict) -> Optional[Expression]: raise FortressException( f"no parser available for {ast['nodeType']} {json.dumps(ast, indent=2)}" )
def convert_yul_unsupported(root: YulScope, parent: YulNode, ast: Dict) -> YulNode: raise FortressException( f"no converter available for {ast['nodeType']} {json.dumps(ast, indent=2)}" )