diff --git a/slither/core/solidity_types/elementary_type.py b/slither/core/solidity_types/elementary_type.py index ce782a5e6..e589054c2 100644 --- a/slither/core/solidity_types/elementary_type.py +++ b/slither/core/solidity_types/elementary_type.py @@ -172,7 +172,7 @@ class ElementaryType(Type): return self.type @property - def size(self) -> Optional[int]: + def size(self) -> int: """ Return the size in bits Return None if the size is not known @@ -190,7 +190,7 @@ class ElementaryType(Type): return int(160) if t.startswith("bytes") and t != "bytes": return int(t[len("bytes") :]) * 8 - return None + raise SlitherException(f"{t} does not have a size") @property def storage_size(self) -> Tuple[int, bool]: diff --git a/slither/tools/read_storage/read_storage.py b/slither/tools/read_storage/read_storage.py index 96365dcf8..5acfb83ed 100644 --- a/slither/tools/read_storage/read_storage.py +++ b/slither/tools/read_storage/read_storage.py @@ -2,7 +2,7 @@ import logging import sys from math import floor from os import environ -from typing import Callable, Optional, Tuple, Union, List, Dict, Any +from typing import Callable, Optional, Tuple, Union, List, Dict, Any, NewType try: from web3 import Web3 @@ -20,12 +20,12 @@ except ImportError: print("$ pip3 install web3 --user\n") sys.exit(-1) -try: - from tabulate import tabulate -except ImportError: - print("ERROR: in order to use slither-read-storage --table, you need to install tabulate") - print("$ pip3 install tabulate --user\n") - sys.exit(-1) +# try: +# from tabulate import tabulate +# except ImportError: +# print("ERROR: in order to use slither-read-storage --table, you need to install tabulate") +# print("$ pip3 install tabulate --user\n") +# sys.exit(-1) from dataclasses import dataclass, field @@ -39,7 +39,8 @@ logging.basicConfig() logger = logging.getLogger("Slither-read-storage") logger.setLevel(logging.INFO) - +Elem = NewType("Elem", Dict[str, "SlotInfo"]) +NestedElem = NewType("NestedElem", Dict[str, Elem]) @dataclass class SlotInfo: type_string: str @@ -48,7 +49,7 @@ class SlotInfo: offset: int value: Optional[Union[int, bool, str, ChecksumAddress]] = None # For structure, str->SlotInfo, for array, str-> SlotInfo - elems: Dict[str, "SlotInfo"] = field(default_factory=lambda: {}) + elems: Elem = field(default_factory=lambda: {}) struct_var: Optional[str] = None @@ -109,7 +110,7 @@ class SlitherReadStorage: def get_storage_layout(self) -> None: """Retrieves the storage layout of entire contract.""" - tmp: Dict[str, SlotInfo] = {} + tmp: Elem for contract, var in self.target_variables: type_ = var.type info = self.get_storage_slot(var, contract) @@ -186,7 +187,7 @@ class SlitherReadStorage: elif isinstance(target_variable_type, MappingType) and key: info, type_to, slot, size, offset = self._find_mapping_slot( - target_variable, slot, key, struct_var=struct_var, deep_key=deep_key + target_variable_type, slot, key, struct_var=struct_var, deep_key=deep_key ) self.log += info @@ -220,9 +221,11 @@ class SlitherReadStorage: struct_var (str): Structure variable name. """ for contract, var in self.target_variables: - self._slot_info[f"{contract.name}.{var.name}"] = self.get_storage_slot( + slot_info = self.get_storage_slot( var, contract, **kwargs ) + if slot_info: + self._slot_info[f"{contract.name}.{var.name}"] = slot_info def get_slot_values(self) -> None: """ @@ -425,7 +428,6 @@ class SlitherReadStorage: slot = int.from_bytes(slot_as_bytes, "big") offset = 0 type_to = "" - size = 0 for var in elems: var_type = var.type if isinstance(var_type, ElementaryType): @@ -477,6 +479,8 @@ class SlitherReadStorage: if isinstance( target_variable_type_type, ArrayType ): # multidimensional array uint[i][], , uint[][i], or uint[][] + assert isinstance(target_variable_type_type.type, ElementaryType) + assert target_variable_type_type.type.size size = target_variable_type_type.type.size type_to = target_variable_type_type.type.name @@ -503,7 +507,7 @@ class SlitherReadStorage: elif target_variable_type.is_fixed_array: slot_int = int.from_bytes(slot, "big") + int(key) - if isinstance(target_variable_type_type, UserDefinedType): # struct[i] + if isinstance(target_variable_type_type, UserDefinedType) and isinstance(target_variable_type_type.type, Structure): # struct[i] type_to = target_variable_type_type.type.name if not struct_var: return info, type_to, int.to_bytes(slot_int, 32, "big"), size, offset @@ -515,10 +519,12 @@ class SlitherReadStorage: info += info_tmp else: + assert isinstance(target_variable_type_type, ElementaryType) type_to = target_variable_type_type.name + assert target_variable_type_type.size size = target_variable_type_type.size # bits - elif isinstance(target_variable_type_type, UserDefinedType): # struct[] + elif isinstance(target_variable_type_type, UserDefinedType) and isinstance(target_variable_type_type.type, Structure): # struct[] slot = keccak(slot) slot_int = int.from_bytes(slot, "big") + int(key) type_to = target_variable_type_type.type.name @@ -532,6 +538,7 @@ class SlitherReadStorage: info += info_tmp else: + assert isinstance(target_variable_type_type, ElementaryType) and target_variable_type_type.size slot = keccak(slot) slot_int = int.from_bytes(slot, "big") + int(key) type_to = target_variable_type_type.name @@ -543,7 +550,7 @@ class SlitherReadStorage: @staticmethod def _find_mapping_slot( - target_variable: StateVariable, + target_variable_type: MappingType, slot: bytes, key: Union[int, str], deep_key: Union[int, str] = None, @@ -557,7 +564,7 @@ class SlitherReadStorage: struct_var (str, optional): Structure variable name. :returns: log (str): Info about the target variable to log. - type_to (bytes): The type of the target variable. + type_to (str): The type of the target variable. slot (bytes): The storage location of the target variable. size (int): The size (in bits) of the target variable. offset (int): The size of other variables that share the same slot. @@ -569,29 +576,30 @@ class SlitherReadStorage: info += f"\nKey: {key}" if deep_key: info += f"\nDeep Key: {deep_key}" - - key_type = target_variable.type.type_from.name + assert isinstance(target_variable_type.type_from, ElementaryType) + key_type = target_variable_type.type_from.name assert key if "int" in key_type: # without this eth_utils encoding fails key = int(key) key = coerce_type(key_type, key) slot = keccak(encode_abi([key_type, "uint256"], [key, decode_single("uint256", slot)])) - if isinstance(target_variable.type.type_to, UserDefinedType) and isinstance( - target_variable.type.type_to.type, Structure + if isinstance(target_variable_type.type_to, UserDefinedType) and isinstance( + target_variable_type.type_to.type, Structure ): # mapping(elem => struct) assert struct_var - elems = target_variable.type.type_to.type.elems_ordered + elems = target_variable_type.type_to.type.elems_ordered info_tmp, type_to, slot, size, offset = SlitherReadStorage._find_struct_var_slot( elems, slot, struct_var ) info += info_tmp elif isinstance( - target_variable.type.type_to, MappingType + target_variable_type.type_to, MappingType ): # mapping(elem => mapping(elem => ???)) assert deep_key - key_type = target_variable.type.type_to.type_from.name + assert isinstance(target_variable_type.type_to.type_from, ElementaryType) + key_type = target_variable_type.type_to.type_from.name if "int" in key_type: # without this eth_utils encoding fails deep_key = int(deep_key) @@ -599,16 +607,18 @@ class SlitherReadStorage: slot = keccak(encode_abi([key_type, "bytes32"], [deep_key, slot])) # mapping(elem => mapping(elem => elem)) - type_to = target_variable.type.type_to.type_to.type - byte_size, _ = target_variable.type.type_to.type_to.storage_size + target_variable_type_type_to_type_to = target_variable_type.type_to.type_to + assert isinstance(target_variable_type_type_to_type_to, (UserDefinedType, ElementaryType)) + type_to = str(target_variable_type_type_to_type_to.type) + byte_size, _ = target_variable_type_type_to_type_to.storage_size size = byte_size * 8 # bits offset = 0 - if isinstance(target_variable.type.type_to.type_to, UserDefinedType) and isinstance( - target_variable.type.type_to.type_to.type, Structure + if isinstance(target_variable_type_type_to_type_to, UserDefinedType) and isinstance( + target_variable_type_type_to_type_to.type, Structure ): # mapping(elem => mapping(elem => struct)) assert struct_var - elems = target_variable.type.type_to.type_to.type.elems_ordered + elems = target_variable_type_type_to_type_to.type.elems_ordered # If map struct, will be bytes32(uint256(keccak256(abi.encode(key1, keccak256(abi.encode(key0, uint(slot)))))) + structFieldDepth); info_tmp, type_to, slot, size, offset = SlitherReadStorage._find_struct_var_slot( elems, slot, struct_var @@ -617,11 +627,15 @@ class SlitherReadStorage: # TODO: suppory mapping with dynamic arrays - else: # mapping(elem => elem) - type_to = target_variable.type.type_to.name # the value's elementary type - byte_size, _ = target_variable.type.type_to.storage_size + # mapping(elem => elem) + elif isinstance(target_variable_type.type_to, ElementaryType): + type_to = target_variable_type.type_to.name # the value's elementary type + byte_size, _ = target_variable_type.type_to.storage_size size = byte_size * 8 # bits + else: + raise NotImplementedError(f"{target_variable_type} => {target_variable_type.type_to} not implemented") + return info, type_to, slot, size, offset @staticmethod @@ -629,6 +643,7 @@ class SlitherReadStorage: contract: Contract, target_variable: StateVariable ) -> Tuple[int, int, int, str]: """Return slot, size, offset, and type.""" + assert isinstance(target_variable.type, Type) type_to = str(target_variable.type) byte_size, _ = target_variable.type.storage_size size = byte_size * 8 # bits @@ -643,7 +658,7 @@ class SlitherReadStorage: @staticmethod def convert_value_to_type( - hex_bytes: HexBytes, size: int, offset: int, type_to: str + hex_bytes: bytes, size: int, offset: int, type_to: str ) -> Union[int, bool, str, ChecksumAddress]: """Convert slot data to type representation.""" # Account for storage packing @@ -657,10 +672,10 @@ class SlitherReadStorage: def _all_struct_slots( self, var: StateVariable, st: Structure, contract: Contract, key: Optional[int] = None - ) -> Dict[str, SlotInfo]: + ) -> Elem: """Retrieves all members of a struct.""" struct_elems = st.elems_ordered - data: Dict[str, SlotInfo] = {} + data: Elem for elem in struct_elems: info = self.get_storage_slot( var, @@ -673,21 +688,18 @@ class SlitherReadStorage: return data - # TODO: remove this exception (montyly) # pylint: disable=too-many-nested-blocks def _all_array_slots( - self, var: StateVariable, contract: Contract, type_: Type, slot: int - ) -> Dict[str, SlotInfo]: + self, var: StateVariable, contract: Contract, type_: ArrayType, slot: int + ) -> Elem: """Retrieves all members of an array.""" array_length = self._get_array_length(type_, slot) - elems: Dict[str, SlotInfo] = {} + elems: Elem + tartget_variable_type = type_.type if isinstance(type_, UserDefinedType): - st = type_.type - if isinstance(st, Structure): + if isinstance(tartget_variable_type, Structure): for i in range(min(array_length, self.max_depth)): - # TODO: figure out why _all_struct_slots returns a Dict[str, SlotInfo] - # but this expect a SlotInfo (montyly) - elems[str(i)] = self._all_struct_slots(var, st, contract, key=i) + elems[str(i)] = self._all_struct_slots(var, tartget_variable_type, contract, key=i) else: for i in range(min(array_length, self.max_depth)): @@ -699,8 +711,8 @@ class SlitherReadStorage: if info: elems[str(i)] = info - if isinstance(type_.type, ArrayType): # multidimensional array - array_length = self._get_array_length(type_.type, info.slot) + if isinstance(tartget_variable_type, ArrayType): # multidimensional array + array_length = self._get_array_length(tartget_variable_type, info.slot) for j in range(min(array_length, self.max_depth)): info = self.get_storage_slot( @@ -710,13 +722,14 @@ class SlitherReadStorage: deep_key=str(j), ) if info: - elems[str(i)].elems[str(j)] = info + elem = elems[str(i)] + elem.elems[str(j)] = info return elems def _get_array_length(self, type_: Type, slot: int) -> int: """Gets the length of dynamic and fixed arrays. Args: - type_ (`Type`): The array type. + type_ (`AbstractType`): The array type. slot (int): Slot a dynamic array's length is stored at. Returns: (int): The length of the array. diff --git a/slither/tools/read_storage/utils/utils.py b/slither/tools/read_storage/utils/utils.py index d9f38d0f0..04cc6518f 100644 --- a/slither/tools/read_storage/utils/utils.py +++ b/slither/tools/read_storage/utils/utils.py @@ -2,10 +2,9 @@ from typing import Union from eth_typing.evm import ChecksumAddress from eth_utils import to_int, to_text, to_checksum_address -from hexbytes import HexBytes -def get_offset_value(hex_bytes: HexBytes, offset: int, size: int) -> bytes: +def get_offset_value(hex_bytes: bytes, offset: int, size: int) -> bytes: """ Trims slot data to only contain the target variable's. Args: @@ -25,7 +24,7 @@ def get_offset_value(hex_bytes: HexBytes, offset: int, size: int) -> bytes: return value -def coerce_type(solidity_type: str, value: bytes) -> Union[int, bool, str, ChecksumAddress]: +def coerce_type(solidity_type: str, value: Union[int, str, bytes]) -> Union[int, bool, str, ChecksumAddress]: """ Converts input to the indicated type. Args: @@ -35,24 +34,27 @@ def coerce_type(solidity_type: str, value: bytes) -> Union[int, bool, str, Check (Union[int, bool, str, ChecksumAddress, hex]): The type representation of the value. """ if "int" in solidity_type: - converted_value = to_int(value) + return to_int(value) elif "bool" in solidity_type: - converted_value = bool(to_int(value)) - elif "string" in solidity_type: + return bool(to_int(value)) + elif "string" in solidity_type and isinstance(value, bytes): # length * 2 is stored in lower end bits # TODO handle bytes and strings greater than 32 bytes length = int(int.from_bytes(value[-2:], "big") / 2) - converted_value = to_text(value[:length]) + return to_text(value[:length]) elif "address" in solidity_type: - converted_value = to_checksum_address(value) + if not isinstance(value, (str, bytes)): + raise TypeError + return to_checksum_address(value) else: - converted_value = value.hex() + if not isinstance(value, bytes): + raise TypeError + return value.hex() - return converted_value -def get_storage_data(web3, checksum_address: ChecksumAddress, slot: bytes) -> HexBytes: +def get_storage_data(web3, checksum_address: ChecksumAddress, slot: bytes) -> bytes: """ Retrieves the storage data from the blockchain at target address and slot. Args: