diff --git a/.github/workflows/read_storage.yml b/.github/workflows/read_storage.yml index eb3bfe9de0..1ce2af7518 100644 --- a/.github/workflows/read_storage.yml +++ b/.github/workflows/read_storage.yml @@ -19,15 +19,14 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout Code - uses: actions/checkout@v2 - - - name: Install ganache - - uses: actions/setup-node@v2 + - uses: actions/checkout@v2 + - name: Setup node + uses: actions/setup-node@v2 with: node-version: '14' - cache: 'npm' - - run: npm install -g ganache + + - name: Install ganache + run: npm install --global ganache - name: Set up Python 3.6 uses: actions/setup-python@v2 @@ -36,9 +35,10 @@ jobs: - name: Install python dependencies run: | - pip install . - pip install web3 - + python3 setup.py install + pip install web3 pytest deepdiff solc-select + solc-select install 0.8.1 + solc-select use 0.8.1 - name: Run tests run: | diff --git a/slither/tools/read_storage/read_storage.py b/slither/tools/read_storage/read_storage.py index 02bace4ed9..75b511e4a8 100644 --- a/slither/tools/read_storage/read_storage.py +++ b/slither/tools/read_storage/read_storage.py @@ -10,7 +10,15 @@ from eth_typing.evm import ChecksumAddress from eth_abi import decode_single, encode_abi from eth_utils import keccak - from .utils import * + from .utils import ( + is_array, + is_mapping, + is_struct, + is_user_defined_type, + get_offset_value, + get_storage_data, + coerce_type, + ) except ImportError: print("ERROR: in order to use slither-read-storage, you need to install web3") print("$ pip3 install web3 --user\n") @@ -31,28 +39,30 @@ class SlitherReadStorageException(Exception): pass -def _all_struct_variables(var, contracts, address, var_name, rpc_url, storage_address, key=None): +# pylint: disable=too-many-arguments +def _all_struct_variables(var, contracts, address, rpc_url, storage_address=None, key=None): """Retrieves all members of a struct.""" if isinstance(var.type.type, StructureContract): struct_elems = var.type.type.elems_ordered else: struct_elems = var.type.type.type.elems_ordered data = {} - for j in range(len(struct_elems)): + for _, elem in enumerate(struct_elems): slot, val, type_string = get_storage_slot_and_val( contracts, address, - var_name, + var.name, rpc_url, storage_address, key=key, - struct_var=struct_elems[j].name, + struct_var=elem.name, ) - data[struct_elems[j].name] = {"slot": slot, "value": val, "type_string": type_string} + data[elem.name] = {"slot": slot, "value": val, "type_string": type_string} return data +# pylint: disable=too-many-branches,too-many-locals,too-many-nested-blocks def get_storage_layout( contracts: List[Contract], address: str, @@ -84,7 +94,7 @@ def get_storage_layout( if is_user_defined_type(type_) and is_struct(type_.type): tmp[var_name]["elems"] = _all_struct_variables( - var, contracts, address, var_name, rpc_url, storage_address + var, contracts, address, rpc_url, storage_address ) continue @@ -100,7 +110,7 @@ def get_storage_layout( if is_user_defined_type(type_.type): for i in range(min(val, max_depth)): elems[i] = _all_struct_variables( - var, contracts, address, var_name, rpc_url, storage_address, key=str(i) + var, contracts, address, rpc_url, storage_address, key=str(i) ) continue @@ -148,6 +158,7 @@ def get_storage_layout( json.dump(data, f, indent=4) +# pylint: disable=too-many-statements,too-many-branches,inconsistent-return-statements def get_storage_slot_and_val( contracts: List[Contract], address: str, @@ -155,7 +166,7 @@ def get_storage_slot_and_val( rpc: str, storage_address: str = None, **kwargs, -) -> Tuple[bytes, Any]: +) -> Tuple[int, Any, str]: """Finds the storage slot of a variable in a contract by its name and retrieves the slot and data. Args: contracts (List[`Contract`]): List of contracts from a slither analyzer object. @@ -168,8 +179,9 @@ def get_storage_slot_and_val( deep_key (str): Key of a mapping embedded within another mapping or secondary index if array. struct_var (str): Structure variable name. Returns: - slot (bytes): The storage location of the variable. + slot (int): The storage location of the variable. value (Any): The type representation of the variable's data. + type_to (str): What type the variable is. Raises: SlitherReadStorageException: if the variable is not found. """ @@ -179,7 +191,6 @@ def get_storage_slot_and_val( address # Default to implementation address unless a storage address is given ) - contract_name = kwargs.get("contract_name", None) key = kwargs.get("key", None) deep_key = kwargs.get("deep_key", None) struct_var = kwargs.get("struct_var", None) @@ -235,25 +246,28 @@ def get_storage_slot_and_val( ) log += info else: + value = get_storage_data(web3, checksum_address, slot).hex() + int_slot = int.from_bytes(slot, byteorder="big") return ( - int.from_bytes(slot, byteorder="big"), - get_storage_data(web3, checksum_address, slot).hex(), + int_slot, + value, type_to, ) elif is_user_defined_type(target_variable.type): if struct_var: var_log_name = struct_var - type_to = target_variable.type.type.name elems = target_variable.type.type.elems_ordered info, type_to, slot, size, offset = _find_struct_var_slot( elems, slot, struct_var ) log += info else: + value = get_storage_data(web3, checksum_address, slot).hex() + int_slot = int.from_bytes(slot, byteorder="big") return ( - int.from_bytes(slot, byteorder="big"), - get_storage_data(web3, checksum_address, slot).hex(), + int_slot, + value, type_to, ) @@ -264,9 +278,11 @@ def get_storage_slot_and_val( ) log += info else: + value = get_storage_data(web3, checksum_address, slot).hex() + int_slot = int.from_bytes(slot, byteorder="big") return ( - int.from_bytes(slot, byteorder="big"), - get_storage_data(web3, checksum_address, slot).hex(), + int_slot, + value, type_to, ) @@ -274,6 +290,8 @@ def get_storage_slot_and_val( type_to = target_variable.type.name hex_bytes = get_storage_data(web3, checksum_address, slot) + int_slot = int.from_bytes(slot, byteorder="big") + # Account for storage packing offset_hex_bytes = get_offset_value(hex_bytes, offset, size) value = coerce_type(type_to, offset_hex_bytes) @@ -281,7 +299,7 @@ def get_storage_slot_and_val( log += f"\nName: {var_log_name}\nType: {type_to}\nValue: {value}\nSlot: {int.from_bytes(slot, byteorder='big')}\n" logger.info(log) - return int.from_bytes(slot, byteorder="big"), value, type_to + return int_slot, value, type_to if not found: raise SlitherReadStorageException("%s was not found in %s" % (var_name, address)) @@ -319,6 +337,7 @@ def _find_struct_var_slot( return info, type_to, slot, size, offset +# pylint: disable=too-many-statements,too-many-branches def _find_array_slot( target_variable: StateVariable, slot: bytes, diff --git a/tests/test_read_storage.py b/tests/test_read_storage.py index fd70a697e1..4e02ecfc3f 100644 --- a/tests/test_read_storage.py +++ b/tests/test_read_storage.py @@ -2,13 +2,13 @@ import os import sys import json -import pytest import shutil import subprocess from time import sleep from typing import Generator + +import pytest from deepdiff import DeepDiff -from dataclasses import dataclass from slither import Slither from slither.tools.read_storage import get_storage_layout @@ -23,11 +23,11 @@ STORAGE_TEST_ROOT = os.path.join(SLITHER_ROOT, "tests", "storage-layout") -@dataclass class GanacheInstance: - provider: str - eth_address: str - eth_privkey: str + def __init__(self, provider: str, eth_address: str, eth_privkey: str): + self.provider = provider + self.eth_address = eth_address + self.eth_privkey = eth_privkey @pytest.fixture(scope="module") @@ -49,7 +49,7 @@ def ganache() -> Generator[GanacheInstance, None, None]: eth_privkey = "0xe48ba530a63326818e116be262fd39ae6dcddd89da4b1f578be8afd4e8894b8d" eth = int(1e18 * 1e6) port = 8545 - p = subprocess.Popen( + with subprocess.Popen( f"""ganache --port {port} --chain.networkId 1 @@ -59,16 +59,16 @@ def ganache() -> Generator[GanacheInstance, None, None]: "\n", " " ), shell=True, - ) + ) as p: - sleep(3) - yield GanacheInstance(f"http://127.0.0.1:{port}", eth_address, eth_privkey) - p.kill() - p.wait() + sleep(3) + yield GanacheInstance(f"http://127.0.0.1:{port}", eth_address, eth_privkey) + p.kill() + p.wait() def get_source_file(file_path): - with open(file_path, "r") as f: + with open(file_path, "r", encoding="utf8") as f: source = f.read() return source @@ -76,7 +76,6 @@ def get_source_file(file_path): def deploy_contract(w3, ganache, contract_bin, contract_abi): """Deploy contract to the local ganache network""" - print("balance", w3.eth.get_balance(ganache.eth_address)) signed_txn = w3.eth.account.sign_transaction( dict( nonce=w3.eth.get_transaction_count(ganache.eth_address), @@ -95,14 +94,15 @@ def deploy_contract(w3, ganache, contract_bin, contract_abi): return contract +# pylint: disable=too-many-locals @pytest.mark.usefixtures("web3", "ganache") def test_read_storage(web3, ganache): assert web3.isConnected() bin_path = os.path.join(STORAGE_TEST_ROOT, "StorageLayout.bin") abi_path = os.path.join(STORAGE_TEST_ROOT, "StorageLayout.abi") - bin = get_source_file(bin_path) + bytecode = get_source_file(bin_path) abi = get_source_file(abi_path) - contract = deploy_contract(web3, ganache, bin, abi) + contract = deploy_contract(web3, ganache, bytecode, abi) contract.functions.store().transact({"from": ganache.eth_address}) address = contract.address @@ -113,9 +113,9 @@ def test_read_storage(web3, ganache): expected_file = os.path.join(STORAGE_TEST_ROOT, "TEST_storage_layout.json") actual_file = os.path.join(SLITHER_ROOT, f"{address}_storage_layout.json") - with open(expected_file, "r") as f: + with open(expected_file, "r", encoding="utf8") as f: expected = json.load(f) - with open(actual_file, "r") as f: + with open(actual_file, "r", encoding="utf8") as f: actual = json.load(f) diff = DeepDiff(expected, actual, ignore_order=True, verbose_level=2, view="tree") @@ -123,9 +123,9 @@ def test_read_storage(web3, ganache): for change in diff.get("values_changed", []): path_list = re.findall(r"\['(.*?)'\]", change.path()) path = "_".join(path_list) - with open(f"{path}_expected.txt", "w") as f: + with open(f"{path}_expected.txt", "w", encoding="utf8") as f: f.write(change.t1) - with open(f"{path}_actual.txt", "w") as f: + with open(f"{path}_actual.txt", "w", encoding="utf8") as f: f.write(change.t2) assert not diff