Merge pull request #1311 from crytic/dev-fix-read-storage

Multiple improvements to slither-read-storage
pull/1343/head
Feist Josselin 2 years ago committed by GitHub
commit ce9dbf650d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .gitignore
  2. 6
      slither/core/solidity_types/elementary_type.py
  3. 43
      slither/tools/read_storage/__main__.py
  4. 365
      slither/tools/read_storage/read_storage.py
  5. 6
      slither/tools/read_storage/utils/__init__.py
  6. 66
      slither/tools/read_storage/utils/utils.py
  7. 259
      tests/storage-layout/TEST_storage_layout.json
  8. 16
      tests/test_read_storage.py

3
.gitignore vendored

@ -107,3 +107,6 @@ ENV/
# Test results
test_artifacts/
# crytic export
crytic-export/

@ -1,5 +1,5 @@
import itertools
from typing import Optional, Tuple
from typing import Tuple
from slither.core.solidity_types.type import Type
@ -176,7 +176,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
@ -194,7 +194,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]:

@ -23,7 +23,7 @@ def parse_args() -> argparse.Namespace:
+ "To retrieve a contract's storage layout:\n"
+ "\tslither-read-storage $TARGET address --contract-name $NAME --layout\n"
+ "To retrieve a contract's storage layout and values:\n"
+ "\tslither-read-storage $TARGET address --contract-name $NAME --layout --values\n"
+ "\tslither-read-storage $TARGET address --contract-name $NAME --layout --value\n"
+ "TARGET can be a contract address or project directory"
),
)
@ -73,9 +73,9 @@ def parse_args() -> argparse.Namespace:
)
parser.add_argument(
"--layout",
action="store_true",
help="Toggle used to write a JSON file with the entire storage layout.",
"--json",
action="store",
help="Save the result in a JSON file.",
)
parser.add_argument(
@ -84,6 +84,18 @@ def parse_args() -> argparse.Namespace:
help="Toggle used to include values in output.",
)
parser.add_argument(
"--table",
action="store_true",
help="Print table view of storage layout",
)
parser.add_argument(
"--silent",
action="store_true",
help="Silence log outputs",
)
parser.add_argument("--max-depth", help="Max depth to search in data structure.", default=20)
cryticparser.init(parser)
@ -120,25 +132,28 @@ def main() -> None:
srs.rpc = args.rpc_url
if args.layout:
srs.get_all_storage_variables()
srs.get_storage_layout()
else:
assert args.variable_name
if args.variable_name:
# Use a lambda func to only return variables that have same name as target.
# x is a tuple (`Contract`, `StateVariable`).
srs.get_all_storage_variables(lambda x: bool(x[1].name == args.variable_name))
srs.get_target_variables(**vars(args))
else:
srs.get_all_storage_variables()
srs.get_storage_layout()
# To retrieve slot values an rpc url is required.
if args.value:
assert args.rpc_url
srs.get_slot_values()
srs.walk_slot_info(srs.get_slot_values)
if args.table:
srs.walk_slot_info(srs.convert_slot_info_to_rows)
print(srs.table)
# Only write file if storage layout is used.
if len(srs.slot_info) > 1:
with open("storage_layout.json", "w", encoding="utf-8") as file:
json.dump(srs.slot_info, file, indent=4)
if args.json:
with open(args.json, "w", encoding="utf-8") as file:
slot_infos_json = srs.to_json()
json.dump(slot_infos_json, file, indent=4)
if __name__ == "__main__":

@ -1,27 +1,14 @@
import sys
import logging
import sys
from math import floor
from typing import Callable, Optional, Tuple, Union, List, Dict
try:
from typing import TypedDict
except ImportError:
# < Python 3.8
from typing_extensions import TypedDict
from typing import Callable, Optional, Tuple, Union, List, Dict, Any
try:
from web3 import Web3
from eth_typing.evm import ChecksumAddress
from eth_abi import decode_single, encode_abi
from eth_utils import keccak
from hexbytes import HexBytes
from .utils import (
is_elementary,
is_array,
is_mapping,
is_struct,
is_user_defined_type,
get_offset_value,
get_storage_data,
coerce_type,
@ -31,25 +18,32 @@ except ImportError:
print("$ pip3 install web3 --user\n")
sys.exit(-1)
import dataclasses
from slither.utils.myprettytable import MyPrettyTable
from slither.core.solidity_types.type import Type
from slither.core.solidity_types import ArrayType
from slither.core.declarations import Contract, StructureContract
from slither.core.solidity_types import ArrayType, ElementaryType, UserDefinedType, MappingType
from slither.core.declarations import Contract, Structure
from slither.core.variables.state_variable import StateVariable
from slither.core.variables.structure_variable import StructureVariable
logging.basicConfig()
logger = logging.getLogger("Slither-read-storage")
logger.setLevel(logging.INFO)
Elem = Dict[str, "SlotInfo"]
NestedElem = Dict[str, Elem]
class SlotInfo(TypedDict):
@dataclasses.dataclass
class SlotInfo:
name: str
type_string: str
slot: int
size: int
offset: int
value: Optional[Union[int, bool, str, ChecksumAddress, hex]]
elems: Optional[TypedDict] # same types as SlotInfo
value: Optional[Union[int, bool, str, ChecksumAddress]] = None
# For structure and array, str->SlotInfo
elems: Union[Elem, NestedElem] = dataclasses.field(default_factory=lambda: {}) # type: ignore[assignment]
class SlitherReadStorageException(Exception):
@ -58,16 +52,17 @@ class SlitherReadStorageException(Exception):
# pylint: disable=too-many-instance-attributes
class SlitherReadStorage:
def __init__(self, contracts, max_depth):
def __init__(self, contracts: List[Contract], max_depth: int) -> None:
self._contracts: List[Contract] = contracts
self._max_depth: int = max_depth
self._log: str = ""
self._slot_info: SlotInfo = {}
self._target_variables = []
self._slot_info: Dict[str, SlotInfo] = {}
self._target_variables: List[Tuple[Contract, StateVariable]] = []
self._web3: Optional[Web3] = None
self._checksum_address: Optional[ChecksumAddress] = None
self.storage_address: Optional[str] = None
self.rpc: Optional[str] = None
self.table: Optional[MyPrettyTable] = None
@property
def contracts(self) -> List[Contract]:
@ -82,7 +77,7 @@ class SlitherReadStorage:
return self._log
@log.setter
def log(self, log) -> str:
def log(self, log: str) -> None:
self._log = log
@property
@ -93,6 +88,8 @@ class SlitherReadStorage:
@property
def checksum_address(self) -> ChecksumAddress:
if not self.storage_address:
raise ValueError
if not self._checksum_address:
self._checksum_address = self.web3.toChecksumAddress(self.storage_address)
return self._checksum_address
@ -103,50 +100,52 @@ class SlitherReadStorage:
return self._target_variables
@property
def slot_info(self) -> SlotInfo:
def slot_info(self) -> Dict[str, SlotInfo]:
"""Contains the location, type, size, offset, and value of contract slots."""
return self._slot_info
def get_storage_layout(self) -> None:
"""Retrieves the storage layout of entire contract."""
tmp = {}
tmp: Dict[str, SlotInfo] = {}
for contract, var in self.target_variables:
type_ = var.type
info = self.get_storage_slot(var, contract)
if info:
tmp[var.name] = info
if isinstance(type_, UserDefinedType) and isinstance(type_.type, Structure):
tmp[var.name].elems = self._all_struct_slots(var, type_.type, contract)
if is_user_defined_type(type_) and is_struct(type_.type):
tmp[var.name]["elems"] = self._all_struct_slots(var, contract)
continue
if is_array(type_):
elems = self._all_array_slots(var, contract, type_, info["slot"])
tmp[var.name]["elems"] = elems
elif isinstance(type_, ArrayType):
elems = self._all_array_slots(var, contract, type_, info.slot)
tmp[var.name].elems = elems
self._slot_info = tmp
# TODO: remove this pylint exception (montyly)
# pylint: disable=too-many-locals
def get_storage_slot(
self,
target_variable: StateVariable,
contract: Contract,
**kwargs,
**kwargs: Any,
) -> Union[SlotInfo, None]:
"""Finds the storage slot of a variable in a given contract.
Args:
target_variable (`StateVariable`): The variable to retrieve the slot for.
contracts (`Contract`): The contract that contains the given state variable.
**kwargs:
key (str): Key of a mapping or index position if an array.
deep_key (str): Key of a mapping embedded within another mapping or secondary index if array.
key (int): Key of a mapping or index position if an array.
deep_key (int): Key of a mapping embedded within another mapping or
secondary index if array.
struct_var (str): Structure variable name.
Returns:
(`SlotInfo`) | None : A dictionary of the slot information.
"""
key = kwargs.get("key", None)
deep_key = kwargs.get("deep_key", None)
struct_var = kwargs.get("struct_var", None)
info = ""
key: Optional[int] = kwargs.get("key", None)
deep_key: Optional[int] = kwargs.get("deep_key", None)
struct_var: Optional[str] = kwargs.get("struct_var", None)
info: str
var_log_name = target_variable.name
try:
int_slot, size, offset, type_to = self.get_variable_info(contract, target_variable)
@ -159,24 +158,33 @@ class SlitherReadStorage:
slot = int.to_bytes(int_slot, 32, byteorder="big")
if is_elementary(target_variable.type):
type_to = target_variable.type.name
target_variable_type = target_variable.type
if isinstance(target_variable_type, ElementaryType):
type_to = target_variable_type.name
elif is_array(target_variable.type) and key:
elif isinstance(target_variable_type, ArrayType) and key is not None:
var_log_name = f"{var_log_name}[{key}]"
info, type_to, slot, size, offset = self._find_array_slot(
target_variable, slot, key, deep_key=deep_key, struct_var=struct_var
target_variable_type,
slot,
key,
deep_key=deep_key,
struct_var=struct_var,
)
self.log += info
elif is_user_defined_type(target_variable.type) and struct_var:
var_log_name = struct_var
elems = target_variable.type.type.elems_ordered
elif isinstance(target_variable_type, UserDefinedType) and struct_var is not None:
var_log_name = f"{var_log_name}.{struct_var}"
target_variable_type_type = target_variable_type.type
assert isinstance(target_variable_type_type, Structure)
elems = target_variable_type_type.elems_ordered
info, type_to, slot, size, offset = self._find_struct_var_slot(elems, slot, struct_var)
self.log += info
elif is_mapping(target_variable.type) and key:
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
@ -184,12 +192,14 @@ class SlitherReadStorage:
self.log += f"\nName: {var_log_name}\nType: {type_to}\nSlot: {int_slot}\n"
logger.info(self.log)
self.log = ""
return {
"type_string": type_to,
"slot": int_slot,
"size": size,
"offset": offset,
}
return SlotInfo(
name=var_log_name,
type_string=type_to,
slot=int_slot,
size=size,
offset=offset,
)
def get_target_variables(self, **kwargs) -> None:
"""
@ -201,28 +211,32 @@ 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(
var, contract, **kwargs
)
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) -> SlotInfo:
"""
Fetches the slot values and inserts them in slot info dictionary.
Returns:
(`SlotInfo`): The dictionary of slot info.
"""
stack = list(self.slot_info.items())
def walk_slot_info(self, func: Callable) -> None:
stack = list(self.slot_info.values())
while stack:
_, v = stack.pop()
if isinstance(v, dict):
stack.extend(v.items())
if "slot" in v:
hex_bytes = get_storage_data(self.web3, self.checksum_address, v["slot"])
v["value"] = self.convert_value_to_type(
hex_bytes, v["size"], v["offset"], v["type_string"]
slot_info = stack.pop()
if isinstance(slot_info, dict): # NestedElem
stack.extend(slot_info.values())
elif slot_info.elems:
stack.extend(list(slot_info.elems.values()))
if isinstance(slot_info, SlotInfo):
func(slot_info)
def get_slot_values(self, slot_info: SlotInfo) -> None:
"""Fetches the slot value of `SlotInfo` object
:param slot_info:
"""
hex_bytes = get_storage_data(
self.web3, self.checksum_address, int.to_bytes(slot_info.slot, 32, byteorder="big")
)
slot_info.value = self.convert_value_to_type(
hex_bytes, slot_info.size, slot_info.offset, slot_info.type_string
)
logger.info(f"\nValue: {v['value']}\n")
return self.slot_info
logger.info(f"\nValue: {slot_info.value}\n")
def get_all_storage_variables(self, func: Callable = None) -> None:
"""Fetches all storage variables from a list of contracts.
@ -241,14 +255,29 @@ class SlitherReadStorage:
)
)
def convert_slot_info_to_rows(self, slot_info: SlotInfo) -> None:
"""Convert and append slot info to table. Create table if it
does not yet exist
:param slot_info:
"""
field_names = [
field.name for field in dataclasses.fields(SlotInfo) if field.name != "elems"
]
if not self.table:
self.table = MyPrettyTable(field_names)
self.table.add_row([getattr(slot_info, field) for field in field_names])
def to_json(self) -> Dict:
return {key: dataclasses.asdict(value) for key, value in self.slot_info.items()}
@staticmethod
def _find_struct_var_slot(
elems: List[StructureVariable], slot: bytes, struct_var: str
elems: List[StructureVariable], slot_as_bytes: bytes, struct_var: str
) -> Tuple[str, str, bytes, int, int]:
"""Finds the slot of a structure variable.
Args:
elems (List[StructureVariable]): Ordered list of structure variables.
slot (bytes): The slot of the struct to begin searching at.
slot_as_bytes (bytes): The slot of the struct to begin searching at.
struct_var (str): The target structure variable.
Returns:
info (str): Info about the target variable to log.
@ -257,31 +286,36 @@ class SlitherReadStorage:
size (int): The size (in bits) of the target variable.
offset (int): The size of other variables that share the same slot.
"""
slot = int.from_bytes(slot, "big")
slot = int.from_bytes(slot_as_bytes, "big")
offset = 0
type_to = ""
for var in elems:
size = var.type.size
var_type = var.type
if isinstance(var_type, ElementaryType):
size = var_type.size
if offset >= 256:
slot += 1
offset = 0
if struct_var == var.name:
type_to = var.type.name
type_to = var_type.name
break # found struct var
offset += size
else:
logger.info(f"{type(var_type)} is current not implemented in _find_struct_var_slot")
slot = int.to_bytes(slot, 32, byteorder="big")
slot_as_bytes = int.to_bytes(slot, 32, byteorder="big")
info = f"\nStruct Variable: {struct_var}"
return info, type_to, slot, size, offset
return info, type_to, slot_as_bytes, size, offset
# pylint: disable=too-many-branches
# pylint: disable=too-many-branches,too-many-statements
@staticmethod
def _find_array_slot(
target_variable: StateVariable,
target_variable_type: ArrayType,
slot: bytes,
key: int,
deep_key: int = None,
struct_var: str = None,
) -> Tuple[str, str, bytes]:
) -> Tuple[str, str, bytes, int, int]:
"""Finds the slot of array's index.
Args:
target_variable (`StateVariable`): The array that contains the target variable.
@ -293,31 +327,36 @@ class SlitherReadStorage:
info (str): Info about the target variable to log.
type_to (str): The type of the target variable.
slot (bytes): The storage location of the target variable.
size (int): The size
offset (int): The offset
"""
info = f"\nKey: {key}"
offset = 0
size = 256
if is_array(
target_variable.type.type
target_variable_type_type = target_variable_type.type
if isinstance(
target_variable_type_type, ArrayType
): # multidimensional array uint[i][], , uint[][i], or uint[][]
size = target_variable.type.type.type.size
type_to = target_variable.type.type.type.name
assert isinstance(target_variable_type_type.type, ElementaryType)
size = target_variable_type_type.type.size
type_to = target_variable_type_type.type.name
if target_variable.type.is_fixed_array: # uint[][i]
if target_variable_type.is_fixed_array: # uint[][i]
slot_int = int.from_bytes(slot, "big") + int(key)
else:
slot = keccak(slot)
key = int(key)
if target_variable.type.type.is_fixed_array: # arr[i][]
key *= int(str(target_variable.type.type.length))
if target_variable_type_type.is_fixed_array: # arr[i][]
key *= int(str(target_variable_type_type.length))
slot_int = int.from_bytes(slot, "big") + key
if not deep_key:
return info, type_to, int.to_bytes(slot_int, 32, "big"), size, offset
info += f"\nDeep Key: {deep_key}"
if target_variable.type.type.is_dynamic_array: # uint[][]
if target_variable_type_type.is_dynamic_array: # uint[][]
# keccak256(keccak256(slot) + index) + floor(j / floor(256 / size))
slot = keccak(int.to_bytes(slot_int, 32, "big"))
slot_int = int.from_bytes(slot, "big")
@ -325,13 +364,15 @@ class SlitherReadStorage:
# keccak256(slot) + index + floor(j / floor(256 / size))
slot_int += floor(int(deep_key) / floor(256 / size)) # uint[i][]
elif target_variable.type.is_fixed_array:
elif target_variable_type.is_fixed_array:
slot_int = int.from_bytes(slot, "big") + int(key)
if is_user_defined_type(target_variable.type.type): # struct[i]
type_to = target_variable.type.type.type.name
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
elems = target_variable.type.type.type.elems_ordered
elems = target_variable_type_type.type.elems_ordered
slot = int.to_bytes(slot_int, 32, byteorder="big")
info_tmp, type_to, slot, size, offset = SlitherReadStorage._find_struct_var_slot(
elems, slot, struct_var
@ -339,16 +380,19 @@ class SlitherReadStorage:
info += info_tmp
else:
type_to = target_variable.type.type.name
size = target_variable.type.type.size # bits
assert isinstance(target_variable_type_type, ElementaryType)
type_to = target_variable_type_type.name
size = target_variable_type_type.size # bits
elif is_user_defined_type(target_variable.type.type): # 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
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
elems = target_variable.type.type.type.elems_ordered
elems = target_variable_type_type.type.elems_ordered
slot = int.to_bytes(slot_int, 32, byteorder="big")
info_tmp, type_to, slot, size, offset = SlitherReadStorage._find_struct_var_slot(
elems, slot, struct_var
@ -356,10 +400,12 @@ class SlitherReadStorage:
info += info_tmp
else:
assert isinstance(target_variable_type_type, ElementaryType)
slot = keccak(slot)
slot_int = int.from_bytes(slot, "big") + int(key)
type_to = target_variable.type.type.name
size = target_variable.type.type.size # bits
type_to = target_variable_type_type.name
size = target_variable_type_type.size # bits
slot = int.to_bytes(slot_int, 32, byteorder="big")
@ -367,7 +413,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,
@ -381,7 +427,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.
@ -393,27 +439,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 is_user_defined_type(target_variable.type.type_to) and is_struct(
target_variable.type.type_to.type
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 is_mapping(target_variable.type.type_to): # mapping(elem => mapping(elem => ???))
elif isinstance(
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)
@ -421,16 +470,20 @@ 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 is_user_defined_type(target_variable.type.type_to.type_to) and is_struct(
target_variable.type.type_to.type_to.type
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
@ -439,11 +492,17 @@ 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
@ -451,6 +510,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
@ -464,8 +524,8 @@ class SlitherReadStorage:
@staticmethod
def convert_value_to_type(
hex_bytes: HexBytes, size: int, offset: int, type_to: str
) -> Union[int, bool, str, ChecksumAddress, hex]:
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
offset_hex_bytes = get_offset_value(hex_bytes, offset, size)
@ -477,14 +537,11 @@ class SlitherReadStorage:
return value
def _all_struct_slots(
self, var: Union[StructureVariable, StructureContract], contract: Contract, key=None
) -> Dict[str, SlotInfo]:
self, var: StateVariable, st: Structure, contract: Contract, key: Optional[int] = None
) -> Elem:
"""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 = {}
struct_elems = st.elems_ordered
data: Elem = {}
for elem in struct_elems:
info = self.get_storage_slot(
var,
@ -492,34 +549,41 @@ class SlitherReadStorage:
key=key,
struct_var=elem.name,
)
if info:
data[elem.name] = info
return data
# pylint: disable=too-many-nested-blocks
def _all_array_slots(
self, var: ArrayType, contract: Contract, type_: Type, slot: int
) -> Dict[int, SlotInfo]:
self, var: StateVariable, contract: Contract, type_: ArrayType, slot: int
) -> Union[Elem, NestedElem]:
"""Retrieves all members of an array."""
array_length = self._get_array_length(type_, slot)
elems = {}
if is_user_defined_type(type_.type):
target_variable_type = type_.type
if isinstance(target_variable_type, UserDefinedType) and isinstance(
target_variable_type.type, Structure
):
nested_elems: NestedElem = {}
for i in range(min(array_length, self.max_depth)):
elems[i] = self._all_struct_slots(var, contract, key=str(i))
continue
nested_elems[str(i)] = self._all_struct_slots(
var, target_variable_type.type, contract, key=i
)
return nested_elems
else:
elems: Elem = {}
for i in range(min(array_length, self.max_depth)):
info = self.get_storage_slot(
var,
contract,
key=str(i),
)
elems[i] = info
if info:
elems[str(i)] = info
if is_array(type_.type): # multidimensional array
array_length = self._get_array_length(type_.type, info["slot"])
if isinstance(target_variable_type, ArrayType): # multidimensional array
array_length = self._get_array_length(target_variable_type, info.slot)
elems[i]["elems"] = {}
for j in range(min(array_length, self.max_depth)):
info = self.get_storage_slot(
var,
@ -527,15 +591,15 @@ class SlitherReadStorage:
key=str(i),
deep_key=str(j),
)
elems[i]["elems"][j] = info
if info:
elems[str(i)].elems[str(j)] = info
return elems
def _get_array_length(self, type_: Type, slot: int = None) -> int:
def _get_array_length(self, type_: Type, slot: int) -> int:
"""Gets the length of dynamic and fixed arrays.
Args:
type_ (`Type`): The array type.
slot (int, optional): Slot a dynamic array's length is stored at.
type_ (`AbstractType`): The array type.
slot (int): Slot a dynamic array's length is stored at.
Returns:
(int): The length of the array.
"""
@ -543,8 +607,13 @@ class SlitherReadStorage:
if self.rpc:
# The length of dynamic arrays is stored at the starting slot.
# Convert from hexadecimal to decimal.
val = int(get_storage_data(self.web3, self.checksum_address, slot).hex(), 16)
if is_array(type_):
val = int(
get_storage_data(
self.web3, self.checksum_address, int.to_bytes(slot, 32, byteorder="big")
).hex(),
16,
)
if isinstance(type_, ArrayType):
if type_.is_fixed_array:
val = int(str(type_.length))

@ -1,10 +1,4 @@
from .utils import (
is_elementary,
is_array,
is_enum,
is_mapping,
is_struct,
is_user_defined_type,
get_offset_value,
get_storage_data,
coerce_type,

@ -1,44 +1,10 @@
from typing import Union
from hexbytes import HexBytes
from eth_typing.evm import ChecksumAddress
from eth_utils import to_int, to_text, to_checksum_address
from slither.core.declarations import Structure, Enum
from slither.core.solidity_types import ArrayType, MappingType, UserDefinedType, ElementaryType
from slither.core.variables.state_variable import StateVariable
def is_elementary(variable: StateVariable) -> bool:
"""Returns whether variable is an elementary type."""
return isinstance(variable, ElementaryType)
def is_array(variable: StateVariable) -> bool:
"""Returns whether variable is an array."""
return isinstance(variable, ArrayType)
def is_mapping(variable: StateVariable) -> bool:
"""Returns whether variable is a mapping."""
return isinstance(variable, MappingType)
def is_struct(variable: StateVariable) -> bool:
"""Returns whether variable is a struct."""
return isinstance(variable, Structure)
def is_enum(variable: StateVariable) -> bool:
"""Returns whether variable is an enum."""
return isinstance(variable, Enum)
def is_user_defined_type(variable: StateVariable) -> bool:
"""Returns whether variable is a struct."""
return isinstance(variable, UserDefinedType)
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:
@ -58,7 +24,9 @@ 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, hex]:
def coerce_type(
solidity_type: str, value: Union[int, str, bytes]
) -> Union[int, bool, str, ChecksumAddress]:
"""
Converts input to the indicated type.
Args:
@ -68,24 +36,26 @@ 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)
elif "bool" in solidity_type:
converted_value = bool(to_int(value))
elif "string" in solidity_type:
return to_int(value)
if "bool" in solidity_type:
return bool(to_int(value))
if "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)
else:
converted_value = value.hex()
if "address" in solidity_type:
if not isinstance(value, (str, bytes)):
raise TypeError
return to_checksum_address(value)
return converted_value
if not isinstance(value, bytes):
raise TypeError
return value.hex()
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:

@ -1,469 +1,576 @@
{
"packedUint": {
"name": "packedUint",
"type_string": "uint248",
"slot": 0,
"size": 248,
"offset": 0,
"value": 1
"value": 1,
"elems": {}
},
"packedBool": {
"name": "packedBool",
"type_string": "bool",
"slot": 0,
"size": 8,
"offset": 248,
"value": true
"value": true,
"elems": {}
},
"_packedStruct": {
"name": "_packedStruct",
"type_string": "StorageLayout.PackedStruct",
"slot": 1,
"size": 256,
"offset": 0,
"value": "0000000000000000000000000000000000000000000000000000000000000101",
"elems": {
"b": {
"name": "_packedStruct.b",
"type_string": "bool",
"slot": 1,
"size": 8,
"offset": 0,
"value": true
"value": true,
"elems": {}
},
"a": {
"name": "_packedStruct.a",
"type_string": "uint248",
"slot": 1,
"size": 248,
"offset": 8,
"value": 1
"value": 1,
"elems": {}
}
}
},
"value": "0000000000000000000000000000000000000000000000000000000000000101"
},
"mappingPackedStruct": {
"name": "mappingPackedStruct",
"type_string": "mapping(uint256 => StorageLayout.PackedStruct)",
"slot": 2,
"size": 256,
"offset": 0,
"value": 0
"value": 0,
"elems": {}
},
"deepMappingPackedStruct": {
"name": "deepMappingPackedStruct",
"type_string": "mapping(address => mapping(uint256 => StorageLayout.PackedStruct))",
"slot": 3,
"size": 256,
"offset": 0,
"value": 0
"value": 0,
"elems": {}
},
"deepMappingElementaryTypes": {
"name": "deepMappingElementaryTypes",
"type_string": "mapping(address => mapping(uint256 => bool))",
"slot": 4,
"size": 256,
"offset": 0,
"value": 0
"value": 0,
"elems": {}
},
"mappingDynamicArrayOfStructs": {
"name": "mappingDynamicArrayOfStructs",
"type_string": "mapping(address => StorageLayout.PackedStruct[])",
"slot": 5,
"size": 256,
"offset": 0,
"value": 0
"value": 0,
"elems": {}
},
"_address": {
"name": "_address",
"type_string": "address",
"slot": 6,
"size": 160,
"offset": 0,
"value": "0xae17D2dD99e07CA3bF2571CCAcEAA9e2Aefc2Dc6"
"value": "0xae17D2dD99e07CA3bF2571CCAcEAA9e2Aefc2Dc6",
"elems": {}
},
"_string": {
"name": "_string",
"type_string": "string",
"slot": 7,
"size": 256,
"offset": 0,
"value": "slither-read-storage"
"value": "slither-read-storage",
"elems": {}
},
"packedUint8": {
"name": "packedUint8",
"type_string": "uint8",
"slot": 8,
"size": 8,
"offset": 0,
"value": 8
"value": 8,
"elems": {}
},
"packedBytes": {
"name": "packedBytes",
"type_string": "bytes8",
"slot": 8,
"size": 64,
"offset": 8,
"value": "6161616161616161"
"value": "6161616161616161",
"elems": {}
},
"_enumA": {
"name": "_enumA",
"type_string": "StorageLayout.Enum",
"slot": 8,
"size": 8,
"offset": 72,
"value": "00"
"value": "00",
"elems": {}
},
"_enumB": {
"name": "_enumB",
"type_string": "StorageLayout.Enum",
"slot": 8,
"size": 8,
"offset": 80,
"value": "01"
"value": "01",
"elems": {}
},
"_enumC": {
"name": "_enumC",
"type_string": "StorageLayout.Enum",
"slot": 8,
"size": 8,
"offset": 88,
"value": "02"
"value": "02",
"elems": {}
},
"fixedArray": {
"name": "fixedArray",
"type_string": "uint256[3]",
"slot": 9,
"size": 768,
"offset": 0,
"value": 1,
"elems": {
"0": {
"name": "fixedArray[0]",
"type_string": "uint256",
"slot": 9,
"size": 256,
"offset": 0,
"value": 1
"value": 1,
"elems": {}
},
"1": {
"name": "fixedArray[1]",
"type_string": "uint256",
"slot": 10,
"size": 256,
"offset": 0,
"value": 2
"value": 2,
"elems": {}
},
"2": {
"name": "fixedArray[2]",
"type_string": "uint256",
"slot": 11,
"size": 256,
"offset": 0,
"value": 3
"value": 3,
"elems": {}
}
}
},
"value": 1
},
"dynamicArrayOfFixedArrays": {
"name": "dynamicArrayOfFixedArrays",
"type_string": "uint256[3][]",
"slot": 12,
"size": 256,
"offset": 0,
"value": 2,
"elems": {
"0": {
"name": "dynamicArrayOfFixedArrays[0]",
"type_string": "uint256",
"slot": 101051993584849178915136821395265346177868384823507754984078593667947067386055,
"size": 256,
"offset": 0,
"value": 1,
"elems": {
"0": {
"name": "dynamicArrayOfFixedArrays[0]",
"type_string": "uint256",
"slot": 101051993584849178915136821395265346177868384823507754984078593667947067386055,
"size": 256,
"offset": 0,
"value": 1
"value": 1,
"elems": {}
},
"1": {
"name": "dynamicArrayOfFixedArrays[0]",
"type_string": "uint256",
"slot": 101051993584849178915136821395265346177868384823507754984078593667947067386056,
"size": 256,
"offset": 0,
"value": 2
"value": 2,
"elems": {}
},
"2": {
"name": "dynamicArrayOfFixedArrays[0]",
"type_string": "uint256",
"slot": 101051993584849178915136821395265346177868384823507754984078593667947067386057,
"size": 256,
"offset": 0,
"value": 3
"value": 3,
"elems": {}
}
}
},
"value": 1
},
"1": {
"name": "dynamicArrayOfFixedArrays[1]",
"type_string": "uint256",
"slot": 101051993584849178915136821395265346177868384823507754984078593667947067386058,
"size": 256,
"offset": 0,
"value": 4,
"elems": {
"0": {
"name": "dynamicArrayOfFixedArrays[1]",
"type_string": "uint256",
"slot": 101051993584849178915136821395265346177868384823507754984078593667947067386058,
"size": 256,
"offset": 0,
"value": 4
"value": 4,
"elems": {}
},
"1": {
"name": "dynamicArrayOfFixedArrays[1]",
"type_string": "uint256",
"slot": 101051993584849178915136821395265346177868384823507754984078593667947067386059,
"size": 256,
"offset": 0,
"value": 5
"value": 5,
"elems": {}
},
"2": {
"name": "dynamicArrayOfFixedArrays[1]",
"type_string": "uint256",
"slot": 101051993584849178915136821395265346177868384823507754984078593667947067386060,
"size": 256,
"offset": 0,
"value": 6
"value": 6,
"elems": {}
}
}
}
},
"value": 4
}
},
"value": 2
},
"fixedArrayofDynamicArrays": {
"name": "fixedArrayofDynamicArrays",
"type_string": "uint256[][3]",
"slot": 13,
"size": 768,
"offset": 0,
"value": 1,
"elems": {
"0": {
"name": "fixedArrayofDynamicArrays[0]",
"type_string": "uint256",
"slot": 13,
"size": 256,
"offset": 0,
"value": 1,
"elems": {
"0": {
"name": "fixedArrayofDynamicArrays[0]",
"type_string": "uint256",
"slot": 97569884605916225051403212656556507955018248777258318895762758024193532305077,
"size": 256,
"offset": 0,
"value": 7
"value": 7,
"elems": {}
}
}
},
"value": 1
},
"1": {
"name": "fixedArrayofDynamicArrays[1]",
"type_string": "uint256",
"slot": 14,
"size": 256,
"offset": 0,
"value": 2,
"elems": {
"0": {
"name": "fixedArrayofDynamicArrays[1]",
"type_string": "uint256",
"slot": 84800337471693920904250232874319843718400766719524250287777680170677855896573,
"size": 256,
"offset": 0,
"value": 8
"value": 8,
"elems": {}
},
"1": {
"name": "fixedArrayofDynamicArrays[1]",
"type_string": "uint256",
"slot": 84800337471693920904250232874319843718400766719524250287777680170677855896574,
"size": 256,
"offset": 0,
"value": 9
"value": 9,
"elems": {}
}
}
},
"value": 2
},
"2": {
"name": "fixedArrayofDynamicArrays[2]",
"type_string": "uint256",
"slot": 15,
"size": 256,
"offset": 0,
"value": 3,
"elems": {
"0": {
"name": "fixedArrayofDynamicArrays[2]",
"type_string": "uint256",
"slot": 63806209331542711802848847270949280092855778197726125910674179583545433573378,
"size": 256,
"offset": 0,
"value": 10
"value": 10,
"elems": {}
},
"1": {
"name": "fixedArrayofDynamicArrays[2]",
"type_string": "uint256",
"slot": 63806209331542711802848847270949280092855778197726125910674179583545433573379,
"size": 256,
"offset": 0,
"value": 11
"value": 11,
"elems": {}
},
"2": {
"name": "fixedArrayofDynamicArrays[2]",
"type_string": "uint256",
"slot": 63806209331542711802848847270949280092855778197726125910674179583545433573380,
"size": 256,
"offset": 0,
"value": 12
"value": 12,
"elems": {}
}
}
}
},
"value": 3
}
},
"value": 1
},
"multidimensionalArray": {
"name": "multidimensionalArray",
"type_string": "uint256[][]",
"slot": 16,
"size": 256,
"offset": 0,
"value": 3,
"elems": {
"0": {
"name": "multidimensionalArray[0]",
"type_string": "uint256",
"slot": 12396694973890998440467380340983585058878106250672390494374587083972727727730,
"size": 256,
"offset": 0,
"value": 1,
"elems": {
"0": {
"name": "multidimensionalArray[0]",
"type_string": "uint256",
"slot": 93856215500098298973000561543003607329881518401177956003908346942307446808932,
"size": 256,
"offset": 0,
"value": 13
"value": 13,
"elems": {}
}
}
},
"value": 1
},
"1": {
"name": "multidimensionalArray[1]",
"type_string": "uint256",
"slot": 12396694973890998440467380340983585058878106250672390494374587083972727727731,
"size": 256,
"offset": 0,
"value": 2,
"elems": {
"0": {
"name": "multidimensionalArray[1]",
"type_string": "uint256",
"slot": 48332168562525185806884758054388614910060623018875025120987491603435926351511,
"size": 256,
"offset": 0,
"value": 14
"value": 14,
"elems": {}
},
"1": {
"name": "multidimensionalArray[1]",
"type_string": "uint256",
"slot": 48332168562525185806884758054388614910060623018875025120987491603435926351512,
"size": 256,
"offset": 0,
"value": 15
"value": 15,
"elems": {}
}
}
},
"value": 2
},
"2": {
"name": "multidimensionalArray[2]",
"type_string": "uint256",
"slot": 12396694973890998440467380340983585058878106250672390494374587083972727727732,
"size": 256,
"offset": 0,
"value": 3,
"elems": {
"0": {
"name": "multidimensionalArray[2]",
"type_string": "uint256",
"slot": 69037578548663760355678879060995014288537668748590083357305779656188235687653,
"size": 256,
"offset": 0,
"value": 16
"value": 16,
"elems": {}
},
"1": {
"name": "multidimensionalArray[2]",
"type_string": "uint256",
"slot": 69037578548663760355678879060995014288537668748590083357305779656188235687654,
"size": 256,
"offset": 0,
"value": 17
"value": 17,
"elems": {}
},
"2": {
"name": "multidimensionalArray[2]",
"type_string": "uint256",
"slot": 69037578548663760355678879060995014288537668748590083357305779656188235687655,
"size": 256,
"offset": 0,
"value": 18
"value": 18,
"elems": {}
}
}
}
},
"value": 3
}
},
"value": 3
},
"dynamicArrayOfStructs": {
"name": "dynamicArrayOfStructs",
"type_string": "StorageLayout.PackedStruct[]",
"slot": 17,
"size": 256,
"offset": 0,
"value": "0000000000000000000000000000000000000000000000000000000000000002",
"elems": {
"0": {
"b": {
"name": "dynamicArrayOfStructs[0]",
"type_string": "bool",
"slot": 22581645139872629890233439717971975110198959689450188087151966948260709403752,
"size": 8,
"offset": 0,
"value": true
"value": true,
"elems": {}
},
"a": {
"name": "dynamicArrayOfStructs[0]",
"type_string": "uint248",
"slot": 22581645139872629890233439717971975110198959689450188087151966948260709403752,
"size": 248,
"offset": 8,
"value": 1
"value": 1,
"elems": {}
}
},
"1": {
"b": {
"name": "dynamicArrayOfStructs[1]",
"type_string": "bool",
"slot": 22581645139872629890233439717971975110198959689450188087151966948260709403753,
"size": 8,
"offset": 0,
"value": false
"value": false,
"elems": {}
},
"a": {
"name": "dynamicArrayOfStructs[1]",
"type_string": "uint248",
"slot": 22581645139872629890233439717971975110198959689450188087151966948260709403753,
"size": 248,
"offset": 8,
"value": 10
"value": 10,
"elems": {}
}
}
}
},
"value": "0000000000000000000000000000000000000000000000000000000000000002"
},
"fixedArrayOfStructs": {
"name": "fixedArrayOfStructs",
"type_string": "StorageLayout.PackedStruct[3]",
"slot": 18,
"size": 768,
"offset": 0,
"value": "0000000000000000000000000000000000000000000000000000000000000101",
"elems": {
"0": {
"b": {
"name": "fixedArrayOfStructs[0]",
"type_string": "bool",
"slot": 18,
"size": 8,
"offset": 0,
"value": true
"value": true,
"elems": {}
},
"a": {
"name": "fixedArrayOfStructs[0]",
"type_string": "uint248",
"slot": 18,
"size": 248,
"offset": 8,
"value": 1
"value": 1,
"elems": {}
}
},
"1": {
"b": {
"name": "fixedArrayOfStructs[1]",
"type_string": "bool",
"slot": 19,
"size": 8,
"offset": 0,
"value": false
"value": false,
"elems": {}
},
"a": {
"name": "fixedArrayOfStructs[1]",
"type_string": "uint248",
"slot": 19,
"size": 248,
"offset": 8,
"value": 10
"value": 10,
"elems": {}
}
},
"2": {
"b": {
"name": "fixedArrayOfStructs[2]",
"type_string": "bool",
"slot": 20,
"size": 8,
"offset": 0,
"value": false
"value": false,
"elems": {}
},
"a": {
"name": "fixedArrayOfStructs[2]",
"type_string": "uint248",
"slot": 20,
"size": 248,
"offset": 8,
"value": 0
"value": 0,
"elems": {}
}
}
}
},
"value": "0000000000000000000000000000000000000000000000000000000000000101"
}
}

@ -14,6 +14,7 @@ from slither.tools.read_storage import SlitherReadStorage
try:
from web3 import Web3
from web3.contract import Contract
except ImportError:
print("ERROR: in order to use slither-read-storage, you need to install web3")
print("$ pip3 install web3 --user\n")
@ -67,14 +68,14 @@ def fixture_ganache() -> Generator[GanacheInstance, None, None]:
p.wait()
def get_source_file(file_path):
def get_source_file(file_path) -> str:
with open(file_path, "r", encoding="utf8") as f:
source = f.read()
return source
def deploy_contract(w3, ganache, contract_bin, contract_abi):
def deploy_contract(w3, ganache, contract_bin, contract_abi) -> Contract:
"""Deploy contract to the local ganache network"""
signed_txn = w3.eth.account.sign_transaction(
dict(
@ -96,7 +97,7 @@ def deploy_contract(w3, ganache, contract_bin, contract_abi):
# pylint: disable=too-many-locals
@pytest.mark.usefixtures("web3", "ganache")
def test_read_storage(web3, ganache):
def test_read_storage(web3, ganache) -> None:
assert web3.isConnected()
bin_path = os.path.join(STORAGE_TEST_ROOT, "StorageLayout.bin")
abi_path = os.path.join(STORAGE_TEST_ROOT, "StorageLayout.abi")
@ -114,9 +115,10 @@ def test_read_storage(web3, ganache):
srs.storage_address = address
srs.get_all_storage_variables()
srs.get_storage_layout()
srs.get_slot_values()
srs.walk_slot_info(srs.get_slot_values)
with open("storage_layout.json", "w", encoding="utf-8") as file:
json.dump(srs.slot_info, file, indent=4)
slot_infos_json = srs.to_json()
json.dump(slot_infos_json, file, indent=4)
expected_file = os.path.join(STORAGE_TEST_ROOT, "TEST_storage_layout.json")
actual_file = os.path.join(SLITHER_ROOT, "storage_layout.json")
@ -132,8 +134,8 @@ def test_read_storage(web3, ganache):
path_list = re.findall(r"\['(.*?)'\]", change.path())
path = "_".join(path_list)
with open(f"{path}_expected.txt", "w", encoding="utf8") as f:
f.write(change.t1)
f.write(str(change.t1))
with open(f"{path}_actual.txt", "w", encoding="utf8") as f:
f.write(change.t2)
f.write(str(change.t2))
assert not diff

Loading…
Cancel
Save