def _set_from_tx(self, tx: Dict) -> None: if not self.sender: self.sender = EthAddress(tx["from"]) self.receiver = EthAddress(tx["to"]) if tx["to"] else None self.value = Wei(tx["value"]) self.gas_price = tx["gasPrice"] self.gas_limit = tx["gas"] self.input = tx["input"] self.nonce = tx["nonce"] # if receiver is a known contract, set function name if not self.fn_name and _find_contract(tx["to"]) is not None: contract = _find_contract(tx["to"]) self.contract_name = contract._name self.fn_name = contract.get_method(tx["input"])
def test_ethaddress_typeerror(): e = EthAddress("0x0063046686E46Dc6F15918b61AE2B121458534a5") with pytest.raises(TypeError): e == "potato" with pytest.raises(TypeError): e == "0x00" assert str(e) != "potato"
def _get_last_map(address: EthAddress, sig: str) -> Dict: contract = state._find_contract(address) last_map = { "address": EthAddress(address), "jumpDepth": 0, "name": None, "coverage": False } if contract: if contract.get_method(sig): full_fn_name = f"{contract._name}.{contract.get_method(sig)}" else: full_fn_name = contract._name last_map.update( contract=contract, function=contract.get_method_object(sig), name=contract._name, internal_calls=[full_fn_name], path_map=contract._build.get("allSourcePaths"), pc_map=contract._build.get("pcMap"), ) if isinstance(contract._project, project_main.Project): # only evaluate coverage for contracts that are part of a `Project` last_map["coverage"] = True if contract._build["language"] == "Solidity": last_map["active_branches"] = set() else: last_map.update(contract=None, internal_calls=[f"<UnknownContract>.{sig}"], pc_map=None) return last_map
def _get_last_map(address: EthAddress, sig: str) -> Dict: contract = _find_contract(address) last_map = { "address": EthAddress(address), "jumpDepth": 0, "name": None, "coverage": False } if contract: if contract.get_method(sig): full_fn_name = f"{contract._name}.{contract.get_method(sig)}" else: full_fn_name = contract._name last_map.update( contract=contract, name=contract._name, fn=[full_fn_name], path_map=contract._build.get("allSourcePaths"), pc_map=contract._build.get("pcMap"), ) if contract._project: last_map["coverage"] = True if contract._build["language"] == "Solidity": last_map["active_branches"] = set() else: last_map.update(contract=None, fn=[f"<UnknownContract>.{sig}"], pc_map=None) return last_map
def _get_last_map(address, sig: str) -> Dict: contract = addr_to_contract[address] if contract == None: # TODO: load abi from explorer return last_map = {"address": EthAddress(address), "jumpDepth": 0, "coverage": False} if contract: if contract.get_method(sig): full_fn_name = f"{contract.address}.{contract.get_method(sig)}" else: full_fn_name = contract.address last_map.update( contract=contract, function=contract.get_method_object(sig), internal_calls=[full_fn_name], # pc_map=contract._build.get("pcMap"), ) # only evaluate coverage for contracts that are part of a `Project` last_map["coverage"] = True # if contract._build["language"] == "Solidity": # last_map["active_branches"] = set() last_map["active_branches"] = set() else: last_map.update(contract=None, internal_calls=[f"<UnknownContract>.{sig}"], pc_map=None) return last_map
def _get_last_map(address: EthAddress, sig: str) -> Dict: contract = _find_contract(address) last_map = {"address": EthAddress(address), "jumpDepth": 0, "name": None} if contract: last_map.update({ "contract": contract, "name": contract._name, "fn": [f"{contract._name}.{contract.get_method(sig)}"], }) if contract._build: last_map["pc_map"] = contract._build["pcMap"] else: last_map.update({"contract": None, "fn": [f"<UnknownContract>.{sig}"]}) return last_map
def get_deployment_address(self, nonce: Optional[int] = None) -> EthAddress: """ Return the address of a contract deployed from this account at the given nonce. Arguments --------- nonce : int, optional The nonce of the deployment transaction. If not given, the nonce of the next transaction is used. """ if nonce is None: nonce = self.nonce address = HexBytes(self.address) raw = rlp.encode([address, nonce]) deployment_address = keccak(raw)[12:] return EthAddress(deployment_address)
def _get_last_map(address: EthAddress, sig: str) -> Dict: contract = _find_contract(address) last_map = { "address": EthAddress(address), "jumpDepth": 0, "name": None, "coverage": False } if contract: last_map.update( contract=contract, name=contract._name, fn=[f"{contract._name}.{contract.get_method(sig)}"], ) if contract._build: last_map.update(pc_map=contract._build["pcMap"], coverage=True) if contract._build["language"] == "Solidity": last_map["active_branches"] = set() else: last_map.update(contract=None, fn=[f"<UnknownContract>.{sig}"]) return last_map
def _add_internal_xfer(self, from_: str, to: str, value: str) -> None: self._internal_transfers.append( # type: ignore {"from": EthAddress(from_), "to": EthAddress(to), "value": Wei(f"0x{value}")} )
def _expand_trace(self) -> None: """Adds the following attributes to each step of the stack trace: address: The address executing this contract. contractName: The name of the contract. fn: The name of the function. jumpDepth: Number of jumps made since entering this contract. The initial value is 0. source: { filename: path to the source file for this step offset: Start and end offset associated source code } """ if self._trace is not None: return if self._raw_trace is None: self._get_trace() self._trace = trace = self._raw_trace self._new_contracts = [] self._internal_transfers = [] if self.contract_address or not trace: coverage._add_transaction(self.coverage_hash, {}) return if "fn" in trace[0]: return if trace[0]["depth"] == 1: self._trace_origin = "geth" self._call_cost = self.gas_used - trace[0]["gas"] + trace[-1]["gas"] for t in trace: t["depth"] = t["depth"] - 1 else: self._trace_origin = "ganache" self._call_cost = trace[0]["gasCost"] for i in range(len(trace) - 1): trace[i]["gasCost"] = trace[i + 1]["gasCost"] trace[-1]["gasCost"] = 0 # last_map gives a quick reference of previous values at each depth last_map = {0: _get_last_map(self.receiver, self.input[:10])} # type: ignore coverage_eval: Dict = {last_map[0]["name"]: {}} for i in range(len(trace)): # if depth has increased, tx has called into a different contract if trace[i]["depth"] > trace[i - 1]["depth"]: step = trace[i - 1] if step["op"] in ("CREATE", "CREATE2"): # creating a new contract out = next(x for x in trace[i:] if x["depth"] == step["depth"]) address = out["stack"][-1][-40:] sig = f"<{step['op']}>" self._new_contracts.append(EthAddress(address)) if int(step["stack"][-1], 16): self._add_internal_xfer(step["address"], address, step["stack"][-1]) else: # calling an existing contract stack_idx = -4 if step["op"] in ("CALL", "CALLCODE") else -3 offset = int(step["stack"][stack_idx], 16) * 2 sig = HexBytes("".join(step["memory"])[offset : offset + 8]).hex() address = step["stack"][-2][-40:] last_map[trace[i]["depth"]] = _get_last_map(address, sig) coverage_eval.setdefault(last_map[trace[i]["depth"]]["name"], {}) # update trace from last_map last = last_map[trace[i]["depth"]] trace[i].update( address=last["address"], contractName=last["name"], fn=last["fn"][-1], jumpDepth=last["jumpDepth"], source=False, ) if trace[i]["op"] == "CALL" and int(trace[i]["stack"][-3], 16): self._add_internal_xfer( last["address"], trace[i]["stack"][-2][-40:], trace[i]["stack"][-3] ) if not last["pc_map"]: continue pc = last["pc_map"][trace[i]["pc"]] if "path" not in pc: continue trace[i]["source"] = {"filename": last["path_map"][pc["path"]], "offset": pc["offset"]} if "fn" not in pc: continue # calculate coverage if last["coverage"]: if pc["path"] not in coverage_eval[last["name"]]: coverage_eval[last["name"]][pc["path"]] = [set(), set(), set()] if "statement" in pc: coverage_eval[last["name"]][pc["path"]][0].add(pc["statement"]) if "branch" in pc: if pc["op"] != "JUMPI": last["active_branches"].add(pc["branch"]) elif "active_branches" not in last or pc["branch"] in last["active_branches"]: # false, true key = 1 if trace[i + 1]["pc"] == trace[i]["pc"] + 1 else 2 coverage_eval[last["name"]][pc["path"]][key].add(pc["branch"]) if "active_branches" in last: last["active_branches"].remove(pc["branch"]) # ignore jumps with no function - they are compiler optimizations if "jump" in pc: # jump 'i' is calling into an internal function if pc["jump"] == "i": try: fn = last["pc_map"][trace[i + 1]["pc"]]["fn"] except (KeyError, IndexError): continue if fn != last["fn"][-1]: last["fn"].append(fn) last["jumpDepth"] += 1 # jump 'o' is returning from an internal function elif last["jumpDepth"] > 0: del last["fn"][-1] last["jumpDepth"] -= 1 coverage._add_transaction( self.coverage_hash, dict((k, v) for k, v in coverage_eval.items() if v) )
def _expand_trace(self) -> None: """Adds the following attributes to each step of the stack trace: address: The address executing this contract. contractName: The name of the contract. fn: The name of the function. jumpDepth: Number of jumps made since entering this contract. The initial value is 0. source: { filename: path to the source file for this step offset: Start and end offset associated source code } """ if self._raw_trace is None: self._get_trace() if self._trace is not None: # in case `_get_trace` also expanded the trace, do not repeat return self._trace = trace = self._raw_trace self._new_contracts = [] self._internal_transfers = [] self._subcalls = [] if self.contract_address or not trace: coverage._add_transaction(self.coverage_hash, {}) return if trace[0]["depth"] == 1: self._trace_origin = "geth" self._call_cost = self.gas_used - trace[0]["gas"] + trace[-1]["gas"] for t in trace: t["depth"] = t["depth"] - 1 else: self._trace_origin = "ganache" if trace[0]["gasCost"] >= 21000: # in ganache <6.10.0, gas costs are shifted by one step - we can # identify this when the first step has a gas cost >= 21000 self._call_cost = trace[0]["gasCost"] for i in range(len(trace) - 1): trace[i]["gasCost"] = trace[i + 1]["gasCost"] trace[-1]["gasCost"] = 0 else: self._call_cost = self.gas_used - trace[0]["gas"] + trace[-1][ "gas"] # last_map gives a quick reference of previous values at each depth last_map = { 0: _get_last_map(self.receiver, self.input[:10]) } # type: ignore coverage_eval: Dict = {last_map[0]["name"]: {}} for i in range(len(trace)): # if depth has increased, tx has called into a different contract if trace[i]["depth"] > trace[i - 1]["depth"]: step = trace[i - 1] if step["op"] in ("CREATE", "CREATE2"): # creating a new contract out = next(x for x in trace[i:] if x["depth"] == step["depth"]) address = out["stack"][-1][-40:] sig = f"<{step['op']}>" calldata = None self._new_contracts.append(EthAddress(address)) if int(step["stack"][-1], 16): self._add_internal_xfer(step["address"], address, step["stack"][-1]) else: # calling an existing contract stack_idx = -4 if step["op"] in ("CALL", "CALLCODE") else -3 offset = int(step["stack"][stack_idx], 16) length = int(step["stack"][stack_idx - 1], 16) calldata = HexBytes("".join( step["memory"]))[offset:offset + length] sig = calldata[:4].hex() address = step["stack"][-2][-40:] last_map[trace[i]["depth"]] = _get_last_map(address, sig) coverage_eval.setdefault(last_map[trace[i]["depth"]]["name"], {}) self._subcalls.append({ "from": step["address"], "to": EthAddress(address), "op": step["op"] }) if step["op"] in ("CALL", "CALLCODE"): self._subcalls[-1]["value"] = int(step["stack"][-3], 16) if calldata and last_map[trace[i]["depth"]].get("function"): fn = last_map[trace[i]["depth"]]["function"] zip_ = zip(fn.abi["inputs"], fn.decode_input(calldata)) self._subcalls[-1].update( inputs={i[0]["name"]: i[1] for i in zip_}, # type:ignore function=fn._input_sig, ) elif calldata: self._subcalls[-1]["calldata"] = calldata.hex() # update trace from last_map last = last_map[trace[i]["depth"]] trace[i].update( address=last["address"], contractName=last["name"], fn=last["internal_calls"][-1], jumpDepth=last["jumpDepth"], source=False, ) opcode = trace[i]["op"] if opcode == "CALL" and int(trace[i]["stack"][-3], 16): self._add_internal_xfer(last["address"], trace[i]["stack"][-2][-40:], trace[i]["stack"][-3]) try: pc = last["pc_map"][trace[i]["pc"]] except (KeyError, TypeError): # we don't have enough information about this contract continue if trace[i]["depth"] and opcode in ("RETURN", "REVERT", "INVALID", "SELFDESTRUCT"): subcall: dict = next( i for i in self._subcalls[::-1] if i["to"] == last["address"] # type: ignore ) if opcode == "RETURN": returndata = _get_memory(trace[i], -1) if returndata: fn = last["function"] try: return_values = fn.decode_output(returndata) if len(fn.abi["outputs"]) == 1: return_values = (return_values, ) subcall["return_value"] = return_values except Exception: subcall["returndata"] = returndata.hex() else: subcall["return_value"] = None elif opcode == "SELFDESTRUCT": subcall["selfdestruct"] = True else: if opcode == "REVERT": data = _get_memory(trace[i], -1)[4:] if data: subcall["revert_msg"] = decode_abi(["string"], data)[0] if "revert_msg" not in subcall and "dev" in pc: subcall["revert_msg"] = pc["dev"] if "path" not in pc: continue trace[i]["source"] = { "filename": last["path_map"][pc["path"]], "offset": pc["offset"] } if "fn" not in pc: continue # calculate coverage if last["coverage"]: if pc["path"] not in coverage_eval[last["name"]]: coverage_eval[last["name"]][pc["path"]] = [ set(), set(), set() ] if "statement" in pc: coverage_eval[last["name"]][pc["path"]][0].add( pc["statement"]) if "branch" in pc: if pc["op"] != "JUMPI": last["active_branches"].add(pc["branch"]) elif "active_branches" not in last or pc["branch"] in last[ "active_branches"]: # false, true key = 1 if trace[i + 1]["pc"] == trace[i]["pc"] + 1 else 2 coverage_eval[last["name"]][pc["path"]][key].add( pc["branch"]) if "active_branches" in last: last["active_branches"].remove(pc["branch"]) # ignore jumps with no function - they are compiler optimizations if "jump" in pc: # jump 'i' is calling into an internal function if pc["jump"] == "i": try: fn = last["pc_map"][trace[i + 1]["pc"]]["fn"] except (KeyError, IndexError): continue if fn != last["internal_calls"][-1]: last["internal_calls"].append(fn) last["jumpDepth"] += 1 # jump 'o' is returning from an internal function elif last["jumpDepth"] > 0: del last["internal_calls"][-1] last["jumpDepth"] -= 1 coverage._add_transaction( self.coverage_hash, dict((k, v) for k, v in coverage_eval.items() if v))
sig = f"<{step['op']}>" calldata = None # self._new_contracts.append(EthAddress(address)) # if int(step["stack"][-1], 16): # self._add_internal_xfer(step["address"], address, step["stack"][-1]) else: # calling an existing contract stack_idx = -4 if step["op"] in ("CALL", "CALLCODE") else -3 offset = int(step["stack"][stack_idx], 16) length = int(step["stack"][stack_idx - 1], 16) calldata = HexBytes("".join(step["memory"]))[offset: offset + length] sig = calldata[:4].hex() address = step["stack"][-2][-40:] depth = trace[i]["depth"] last_map[depth] = _get_last_map(EthAddress(address), sig) coverage_eval.setdefault(last_map[trace[i]["depth"]]["address"], {}) if receipt._subcalls == None: receipt._subcalls = [] receipt._subcalls.append( {"from": step["address"], "to": EthAddress(address), "op": step["op"]} ) if step["op"] in ("CALL", "CALLCODE"): receipt._subcalls[-1]["value"] = int(step["stack"][-3], 16) if calldata and last_map[trace[i]["depth"]].get("function"): fn = last_map[trace[i]["depth"]]["function"] receipt._subcalls[-1]["function"] = fn._input_sig try: zip_ = zip(fn.abi["inputs"], fn.decode_input(calldata)) inputs = {i[0]["name"]: i[1] for i in zip_} # type: ignore