feat: add `--unstructured` to slither-read-storage; retrieve custom storage layouts (#1963)

Retrieve storage values of constant arguments passed to SLOAD. If a call to keccack is made, the expression is evaluated and treated as a constant.
---------

Co-authored-by: webthethird <ratzbodell@gmail.com>
pull/1977/head
alpharush 1 year ago committed by GitHub
parent 8a5aab62c9
commit cec07db510
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      slither/printers/guidance/echidna.py
  2. 2
      slither/solc_parsing/expressions/expression_parsing.py
  3. 7
      slither/tools/read_storage/__main__.py
  4. 315
      slither/tools/read_storage/read_storage.py
  5. 2
      slither/tools/read_storage/utils/utils.py
  6. 4
      slither/utils/integer_conversion.py
  7. 71
      slither/visitors/expression/constants_folding.py
  8. 6
      tests/conftest.py
  9. 0
      tests/tools/check_erc/erc20.sol
  10. 0
      tests/tools/check_erc/test_1.txt
  11. 0
      tests/tools/check_kspec/safeAdd/safeAdd.sol
  12. 0
      tests/tools/check_kspec/safeAdd/spec.md
  13. 0
      tests/tools/check_kspec/test_1.txt
  14. 0
      tests/tools/check_upgradeability/contractV1.sol
  15. 0
      tests/tools/check_upgradeability/contractV1_struct.sol
  16. 0
      tests/tools/check_upgradeability/contractV2.sol
  17. 0
      tests/tools/check_upgradeability/contractV2_bug.sol
  18. 0
      tests/tools/check_upgradeability/contractV2_bug2.sol
  19. 0
      tests/tools/check_upgradeability/contractV2_struct.sol
  20. 0
      tests/tools/check_upgradeability/contractV2_struct_bug.sol
  21. 0
      tests/tools/check_upgradeability/contract_initialization.sol
  22. 0
      tests/tools/check_upgradeability/contract_v1_var_init.sol
  23. 0
      tests/tools/check_upgradeability/contract_v2_constant.sol
  24. 0
      tests/tools/check_upgradeability/proxy.sol
  25. 0
      tests/tools/check_upgradeability/test_1.txt
  26. 0
      tests/tools/check_upgradeability/test_10.txt
  27. 0
      tests/tools/check_upgradeability/test_11.txt
  28. 0
      tests/tools/check_upgradeability/test_12.txt
  29. 0
      tests/tools/check_upgradeability/test_13.txt
  30. 0
      tests/tools/check_upgradeability/test_2.txt
  31. 0
      tests/tools/check_upgradeability/test_3.txt
  32. 0
      tests/tools/check_upgradeability/test_4.txt
  33. 0
      tests/tools/check_upgradeability/test_5.txt
  34. 0
      tests/tools/check_upgradeability/test_6.txt
  35. 0
      tests/tools/check_upgradeability/test_7.txt
  36. 0
      tests/tools/check_upgradeability/test_8.txt
  37. 0
      tests/tools/check_upgradeability/test_9.txt
  38. 56
      tests/tools/read-storage/conftest.py
  39. 3
      tests/tools/read-storage/test_data/StorageLayout.sol
  40. 56
      tests/tools/read-storage/test_data/TEST_unstructured_storage.json
  41. 1
      tests/tools/read-storage/test_data/UnstructuredStorageLayout.abi
  42. 1
      tests/tools/read-storage/test_data/UnstructuredStorageLayout.bin
  43. 141
      tests/tools/read-storage/test_data/UnstructuredStorageLayout.sol
  44. 66
      tests/tools/read-storage/test_read_storage.py
  45. 59
      tests/unit/core/test_constant_folding.py
  46. 2
      tests/unit/core/test_data/constant_folding/constant_folding_binop.sol

@ -32,7 +32,7 @@ from slither.slithir.operations import (
from slither.slithir.operations.binary import Binary
from slither.slithir.variables import Constant
from slither.utils.output import Output
from slither.visitors.expression.constants_folding import ConstantFolding
from slither.visitors.expression.constants_folding import ConstantFolding, NotConstant
def _get_name(f: Union[Function, Variable]) -> str:
@ -178,11 +178,16 @@ def _extract_constants_from_irs( # pylint: disable=too-many-branches,too-many-n
all_cst_used_in_binary[str(ir.type)].append(
ConstantValue(str(r.value), str(r.type))
)
if isinstance(ir.variable_left, Constant) and isinstance(ir.variable_right, Constant):
if ir.lvalue:
type_ = ir.lvalue.type
cst = ConstantFolding(ir.expression, type_).result()
all_cst_used.append(ConstantValue(str(cst.value), str(type_)))
if isinstance(ir.variable_left, Constant) or isinstance(
ir.variable_right, Constant
):
if ir.lvalue:
try:
type_ = ir.lvalue.type
cst = ConstantFolding(ir.expression, type_).result()
all_cst_used.append(ConstantValue(str(cst.value), str(type_)))
except NotConstant:
pass
if isinstance(ir, TypeConversion):
if isinstance(ir.variable, Constant):
if isinstance(ir.type, TypeAlias):

@ -433,6 +433,8 @@ def parse_expression(expression: Dict, caller_context: CallerContextExpression)
type_candidate = ElementaryType("uint256")
else:
type_candidate = ElementaryType("string")
elif type_candidate.startswith("rational_const "):
type_candidate = ElementaryType("uint256")
elif type_candidate.startswith("int_const "):
type_candidate = ElementaryType("uint256")
elif type_candidate.startswith("bool"):

@ -104,6 +104,12 @@ def parse_args() -> argparse.Namespace:
default="latest",
)
parser.add_argument(
"--unstructured",
action="store_true",
help="Include unstructured storage slots",
)
cryticparser.init(parser)
return parser.parse_args()
@ -133,6 +139,7 @@ def main() -> None:
rpc_info = RpcInfo(args.rpc_url, block)
srs = SlitherReadStorage(contracts, args.max_depth, rpc_info)
srs.unstructured = bool(args.unstructured)
# Remove target prefix e.g. rinkeby:0x0 -> 0x0.
address = target[target.find(":") + 1 :]
# Default to implementation address unless a storage address is given.

@ -15,9 +15,21 @@ from web3.middleware import geth_poa_middleware
from slither.core.declarations import Contract, Structure
from slither.core.solidity_types import ArrayType, ElementaryType, MappingType, UserDefinedType
from slither.core.solidity_types.type import Type
from slither.core.cfg.node import NodeType
from slither.core.variables.state_variable import StateVariable
from slither.core.variables.structure_variable import StructureVariable
from slither.core.expressions import (
AssignmentOperation,
Literal,
Identifier,
BinaryOperation,
UnaryOperation,
TupleExpression,
TypeConversion,
CallExpression,
)
from slither.utils.myprettytable import MyPrettyTable
from slither.visitors.expression.constants_folding import ConstantFolding, NotConstant
from .utils import coerce_type, get_offset_value, get_storage_data
@ -72,7 +84,7 @@ class RpcInfo:
return self._block
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-instance-attributes,too-many-public-methods
class SlitherReadStorage:
def __init__(self, contracts: List[Contract], max_depth: int, rpc_info: RpcInfo = None) -> None:
self._checksum_address: Optional[ChecksumAddress] = None
@ -81,9 +93,11 @@ class SlitherReadStorage:
self._max_depth: int = max_depth
self._slot_info: Dict[str, SlotInfo] = {}
self._target_variables: List[Tuple[Contract, StateVariable]] = []
self._constant_storage_slots: List[Tuple[Contract, StateVariable]] = []
self.rpc_info: Optional[RpcInfo] = rpc_info
self.storage_address: Optional[str] = None
self.table: Optional[MyPrettyTable] = None
self.unstructured: bool = False
@property
def contracts(self) -> List[Contract]:
@ -114,6 +128,11 @@ class SlitherReadStorage:
"""Storage variables (not constant or immutable) and their associated contract."""
return self._target_variables
@property
def constant_slots(self) -> List[Tuple[Contract, StateVariable]]:
"""Constant bytes32 variables and their associated contract."""
return self._constant_storage_slots
@property
def slot_info(self) -> Dict[str, SlotInfo]:
"""Contains the location, type, size, offset, and value of contract slots."""
@ -133,9 +152,48 @@ class SlitherReadStorage:
elif isinstance(type_, ArrayType):
elems = self._all_array_slots(var, contract, type_, info.slot)
tmp[var.name].elems = elems
if self.unstructured:
tmp.update(self.get_unstructured_layout())
self._slot_info = tmp
def get_unstructured_layout(self) -> Dict[str, SlotInfo]:
tmp: Dict[str, SlotInfo] = {}
for _, var in self.constant_slots:
var_name = var.name
try:
exp = var.expression
if isinstance(
exp,
(
BinaryOperation,
UnaryOperation,
Identifier,
TupleExpression,
TypeConversion,
CallExpression,
),
):
exp = ConstantFolding(exp, "bytes32").result()
if isinstance(exp, Literal):
slot = coerce_type("int", exp.value)
else:
continue
offset = 0
type_string, size = self.find_constant_slot_storage_type(var)
if type_string:
tmp[var.name] = SlotInfo(
name=var_name, type_string=type_string, slot=slot, size=size, offset=offset
)
self.log += (
f"\nSlot Name: {var_name}\nType: bytes32"
f"\nStorage Type: {type_string}\nSlot: {str(exp)}\n"
)
logger.info(self.log)
self.log = ""
except NotConstant:
continue
return tmp
# TODO: remove this pylint exception (montyly)
# pylint: disable=too-many-locals
def get_storage_slot(
@ -144,7 +202,8 @@ class SlitherReadStorage:
contract: Contract,
**kwargs: Any,
) -> Union[SlotInfo, None]:
"""Finds the storage slot of a variable in a given contract.
"""
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.
@ -230,6 +289,78 @@ class SlitherReadStorage:
if slot_info:
self._slot_info[f"{contract.name}.{var.name}"] = slot_info
def find_constant_slot_storage_type(
self, var: StateVariable
) -> Tuple[Optional[str], Optional[int]]:
"""
Given a constant bytes32 StateVariable, tries to determine which variable type is stored there, using the
heuristic that if a function reads from the slot and returns a value, it probably stores that type of value.
Also uses the StorageSlot library as a heuristic when a function has no return but uses the library's getters.
Args:
var (StateVariable): The constant bytes32 storage slot.
Returns:
type (str): The type of value stored in the slot.
size (int): The type's size in bits.
"""
assert var.is_constant and var.type == ElementaryType("bytes32")
storage_type = None
size = None
funcs = []
for c in self.contracts:
c_funcs = c.get_functions_reading_from_variable(var)
c_funcs.extend(
f
for f in c.functions
if any(str(v.expression) == str(var.expression) for v in f.variables)
)
c_funcs = list(set(c_funcs))
funcs.extend(c_funcs)
fallback = [f for f in var.contract.functions if f.is_fallback]
funcs += fallback
for func in funcs:
rets = func.return_type if func.return_type is not None else []
for ret in rets:
size, _ = ret.storage_size
if size <= 32:
return str(ret), size * 8
for node in func.all_nodes():
exp = node.expression
# Look for use of the common OpenZeppelin StorageSlot library
if f"getAddressSlot({var.name})" in str(exp):
return "address", 160
if f"getBooleanSlot({var.name})" in str(exp):
return "bool", 1
if f"getBytes32Slot({var.name})" in str(exp):
return "bytes32", 256
if f"getUint256Slot({var.name})" in str(exp):
return "uint256", 256
# Look for variable assignment in assembly loaded from a hardcoded slot
if (
isinstance(exp, AssignmentOperation)
and isinstance(exp.expression_left, Identifier)
and isinstance(exp.expression_right, CallExpression)
and "sload" in str(exp.expression_right.called)
and str(exp.expression_right.arguments[0]) == str(var.expression)
):
if func.is_fallback:
return "address", 160
storage_type = exp.expression_left.value.type.name
size, _ = exp.expression_left.value.type.storage_size
return storage_type, size * 8
# Look for variable storage in assembly stored to a hardcoded slot
if (
isinstance(exp, CallExpression)
and "sstore" in str(exp.called)
and isinstance(exp.arguments[0], Identifier)
and isinstance(exp.arguments[1], Identifier)
and str(exp.arguments[0].value.expression) == str(var.expression)
):
storage_type = exp.arguments[1].value.type.name
size, _ = exp.arguments[1].value.type.storage_size
return storage_type, size * 8
return storage_type, size
def walk_slot_info(self, func: Callable) -> None:
stack = list(self.slot_info.values())
while stack:
@ -242,7 +373,8 @@ class SlitherReadStorage:
func(slot_info)
def get_slot_values(self, slot_info: SlotInfo) -> None:
"""Fetches the slot value of `SlotInfo` object
"""
Fetches the slot value of `SlotInfo` object
:param slot_info:
"""
assert self.rpc_info is not None
@ -257,25 +389,162 @@ class SlitherReadStorage:
)
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.
def get_all_storage_variables(self, func: Callable = lambda x: x) -> None:
"""
Fetches all storage variables from a list of contracts.
kwargs:
func (Callable, optional): A criteria to filter functions e.g. name.
"""
for contract in self.contracts:
self._target_variables.extend(
filter(
func,
[
(contract, var)
for var in contract.state_variables_ordered
if not var.is_constant and not var.is_immutable
],
)
for var in contract.state_variables_ordered:
if func(var):
if not var.is_constant and not var.is_immutable:
self._target_variables.append((contract, var))
elif (
self.unstructured
and var.is_constant
and var.type == ElementaryType("bytes32")
):
self._constant_storage_slots.append((contract, var))
if self.unstructured:
hardcoded_slot = self.find_hardcoded_slot_in_fallback(contract)
if hardcoded_slot is not None:
self._constant_storage_slots.append((contract, hardcoded_slot))
def find_hardcoded_slot_in_fallback(self, contract: Contract) -> Optional[StateVariable]:
"""
Searches the contract's fallback function for a sload from a literal storage slot, i.e.,
`let contractLogic := sload(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7)`.
Args:
contract: a Contract object, which should have a fallback function.
Returns:
A newly created StateVariable representing the Literal bytes32 slot, if one is found, otherwise None.
"""
fallback = None
for func in contract.functions_entry_points:
if func.is_fallback:
fallback = func
break
if fallback is None:
return None
queue = [fallback.entry_point]
visited = []
while len(queue) > 0:
node = queue.pop(0)
visited.append(node)
queue.extend(son for son in node.sons if son not in visited)
if node.type == NodeType.ASSEMBLY and isinstance(node.inline_asm, str):
return SlitherReadStorage.find_hardcoded_slot_in_asm_str(node.inline_asm, contract)
if node.type == NodeType.EXPRESSION:
sv = self.find_hardcoded_slot_in_exp(node.expression, contract)
if sv is not None:
return sv
return None
@staticmethod
def find_hardcoded_slot_in_asm_str(
inline_asm: str, contract: Contract
) -> Optional[StateVariable]:
"""
Searches a block of assembly code (given as a string) for a sload from a literal storage slot.
Does not work if the argument passed to sload does not start with "0x", i.e., `sload(add(1,1))`
or `and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff)`.
Args:
inline_asm: a string containing all the code in an assembly node (node.inline_asm for solc < 0.6.0).
Returns:
A newly created StateVariable representing the Literal bytes32 slot, if one is found, otherwise None.
"""
asm_split = inline_asm.split("\n")
for asm in asm_split:
if "sload(" in asm: # Only handle literals
arg = asm.split("sload(")[1].split(")")[0]
if arg.startswith("0x"):
exp = Literal(arg, ElementaryType("bytes32"))
sv = StateVariable()
sv.name = "fallback_sload_hardcoded"
sv.expression = exp
sv.is_constant = True
sv.type = exp.type
sv.set_contract(contract)
return sv
return None
def find_hardcoded_slot_in_exp(
self, exp: "Expression", contract: Contract
) -> Optional[StateVariable]:
"""
Parses an expression to see if it contains a sload from a literal storage slot,
unrolling nested expressions if necessary to determine which slot it loads from.
Args:
exp: an Expression object to search.
contract: the Contract containing exp.
Returns:
A newly created StateVariable representing the Literal bytes32 slot, if one is found, otherwise None.
"""
if isinstance(exp, AssignmentOperation):
exp = exp.expression_right
while isinstance(exp, BinaryOperation):
exp = next(
(e for e in exp.expressions if isinstance(e, (CallExpression, BinaryOperation))),
exp.expression_left,
)
while isinstance(exp, CallExpression) and len(exp.arguments) > 0:
called = exp.called
exp = exp.arguments[0]
if "sload" in str(called):
break
if isinstance(
exp,
(
BinaryOperation,
UnaryOperation,
Identifier,
TupleExpression,
TypeConversion,
CallExpression,
),
):
try:
exp = ConstantFolding(exp, "bytes32").result()
except NotConstant:
return None
if (
isinstance(exp, Literal)
and isinstance(exp.type, ElementaryType)
and exp.type.name in ["bytes32", "uint256"]
):
sv = StateVariable()
sv.name = "fallback_sload_hardcoded"
value = exp.value
str_value = str(value)
if str_value.isdecimal():
value = int(value)
if isinstance(value, (int, bytes)):
if isinstance(value, bytes):
str_value = "0x" + value.hex()
value = int(str_value, 16)
exp = Literal(str_value, ElementaryType("bytes32"))
state_var_slots = [
self.get_variable_info(contract, var)[0]
for contract, var in self.target_variables
]
if value in state_var_slots:
return None
sv.expression = exp
sv.is_constant = True
sv.type = ElementaryType("bytes32")
sv.set_contract(contract)
return sv
return None
def convert_slot_info_to_rows(self, slot_info: SlotInfo) -> None:
"""Convert and append slot info to table. Create table if it
"""
Convert and append slot info to table. Create table if it
does not yet exist
:param slot_info:
"""
@ -293,7 +562,8 @@ class SlitherReadStorage:
def _find_struct_var_slot(
elems: List[StructureVariable], slot_as_bytes: bytes, struct_var: str
) -> Tuple[str, str, bytes, int, int]:
"""Finds the slot of a structure variable.
"""
Finds the slot of a structure variable.
Args:
elems (List[StructureVariable]): Ordered list of structure variables.
slot_as_bytes (bytes): The slot of the struct to begin searching at.
@ -335,7 +605,8 @@ class SlitherReadStorage:
deep_key: int = None,
struct_var: str = None,
) -> Tuple[str, str, bytes, int, int]:
"""Finds the slot of array's index.
"""
Finds the slot of array's index.
Args:
target_variable (`StateVariable`): The array that contains the target variable.
slot (bytes): The starting slot of the array.
@ -438,7 +709,8 @@ class SlitherReadStorage:
deep_key: Union[int, str] = None,
struct_var: str = None,
) -> Tuple[str, str, bytes, int, int]:
"""Finds the data slot of a target variable within a mapping.
"""
Finds the data slot of a target variable within a mapping.
target_variable (`StateVariable`): The mapping that contains the target variable.
slot (bytes): The starting slot of the mapping.
key (Union[int, str]): The key the variable is stored at.
@ -509,7 +781,7 @@ class SlitherReadStorage:
)
info += info_tmp
# TODO: suppory mapping with dynamic arrays
# TODO: support mapping with dynamic arrays
# mapping(elem => elem)
elif isinstance(target_variable_type.type_to, ElementaryType):
@ -615,7 +887,8 @@ class SlitherReadStorage:
return elems
def _get_array_length(self, type_: Type, slot: int) -> int:
"""Gets the length of dynamic and fixed arrays.
"""
Gets the length of dynamic and fixed arrays.
Args:
type_ (`AbstractType`): The array type.
slot (int): Slot a dynamic array's length is stored at.

@ -37,6 +37,8 @@ def coerce_type(
(Union[int, bool, str, ChecksumAddress, hex]): The type representation of the value.
"""
if "int" in solidity_type:
if str(value).startswith("0x"):
return to_int(hexstr=value)
return to_int(value)
if "bool" in solidity_type:
return bool(to_int(value))

@ -4,7 +4,9 @@ from typing import Union
from slither.exceptions import SlitherError
def convert_string_to_fraction(val: Union[str, int]) -> Fraction:
def convert_string_to_fraction(val: Union[str, bytes, int]) -> Fraction:
if isinstance(val, bytes):
return int.from_bytes(val, byteorder="big")
if isinstance(val, int):
return Fraction(val)
if val.startswith(("0x", "0X")):

@ -1,5 +1,6 @@
from fractions import Fraction
from typing import Union
from Crypto.Hash import keccak
from slither.core import expressions
from slither.core.expressions import (
@ -11,9 +12,9 @@ from slither.core.expressions import (
UnaryOperation,
TupleExpression,
TypeConversion,
CallExpression,
)
from slither.core.variables import Variable
from slither.utils.integer_conversion import convert_string_to_fraction, convert_string_to_int
from slither.visitors.expression.expression import ExpressionVisitor
from slither.core.solidity_types.elementary_type import ElementaryType
@ -65,23 +66,31 @@ class ConstantFolding(ExpressionVisitor):
value = value & (2**256 - 1)
return Literal(value, self._type)
# pylint: disable=import-outside-toplevel
def _post_identifier(self, expression: Identifier) -> None:
if not isinstance(expression.value, Variable):
return
if not expression.value.is_constant:
from slither.core.declarations.solidity_variables import SolidityFunction
if isinstance(expression.value, Variable):
if expression.value.is_constant:
expr = expression.value.expression
# assumption that we won't have infinite loop
# Everything outside of literal
if isinstance(
expr,
(BinaryOperation, UnaryOperation, Identifier, TupleExpression, TypeConversion),
):
cf = ConstantFolding(expr, self._type)
expr = cf.result()
assert isinstance(expr, Literal)
set_val(expression, convert_string_to_int(expr.converted_value))
else:
raise NotConstant
elif isinstance(expression.value, SolidityFunction):
set_val(expression, expression.value)
else:
raise NotConstant
expr = expression.value.expression
# assumption that we won't have infinite loop
# Everything outside of literal
if isinstance(
expr, (BinaryOperation, UnaryOperation, Identifier, TupleExpression, TypeConversion)
):
cf = ConstantFolding(expr, self._type)
expr = cf.result()
assert isinstance(expr, Literal)
set_val(expression, convert_string_to_int(expr.converted_value))
# pylint: disable=too-many-branches
# pylint: disable=too-many-branches,too-many-statements
def _post_binary_operation(self, expression: BinaryOperation) -> None:
expression_left = expression.expression_left
expression_right = expression.expression_right
@ -95,7 +104,6 @@ class ConstantFolding(ExpressionVisitor):
(Literal, BinaryOperation, UnaryOperation, Identifier, TupleExpression, TypeConversion),
):
raise NotConstant
left = get_val(expression_left)
right = get_val(expression_right)
@ -183,7 +191,9 @@ class ConstantFolding(ExpressionVisitor):
raise NotConstant
def _post_literal(self, expression: Literal) -> None:
if expression.converted_value in ["true", "false"]:
if str(expression.type) == "bool":
set_val(expression, expression.converted_value)
elif str(expression.type) == "string":
set_val(expression, expression.converted_value)
else:
try:
@ -195,7 +205,14 @@ class ConstantFolding(ExpressionVisitor):
raise NotConstant
def _post_call_expression(self, expression: expressions.CallExpression) -> None:
raise NotConstant
called = get_val(expression.called)
args = [get_val(arg) for arg in expression.arguments]
if called.name == "keccak256(bytes)":
digest = keccak.new(digest_bits=256)
digest.update(str(args[0]).encode("utf-8"))
set_val(expression, digest.digest())
else:
raise NotConstant
def _post_conditional_expression(self, expression: expressions.ConditionalExpression) -> None:
raise NotConstant
@ -247,10 +264,24 @@ class ConstantFolding(ExpressionVisitor):
expr = expression.expression
if not isinstance(
expr,
(Literal, BinaryOperation, UnaryOperation, Identifier, TupleExpression, TypeConversion),
(
Literal,
BinaryOperation,
UnaryOperation,
Identifier,
TupleExpression,
TypeConversion,
CallExpression,
),
):
raise NotConstant
cf = ConstantFolding(expr, self._type)
expr = cf.result()
assert isinstance(expr, Literal)
set_val(expression, convert_string_to_fraction(expr.converted_value))
if str(expression.type).startswith("uint") and isinstance(expr.value, bytes):
value = int.from_bytes(expr.value, "big")
elif str(expression.type).startswith("byte") and isinstance(expr.value, int):
value = int.to_bytes(expr.value, 32, "big")
else:
value = convert_string_to_fraction(expr.converted_value)
set_val(expression, value)

@ -1,12 +1,12 @@
# pylint: disable=redefined-outer-name
import os
from pathlib import Path
import tempfile
import shutil
import tempfile
from pathlib import Path
from contextlib import contextmanager
import pytest
from filelock import FileLock
from solc_select import solc_select
import pytest
from slither import Slither

@ -0,0 +1,56 @@
"""
Testing utilities for the read-storage tool
"""
import shutil
import subprocess
from time import sleep
from typing import Generator
from dataclasses import dataclass
from web3 import Web3
import pytest
@dataclass
class GanacheInstance:
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", name="ganache")
def fixture_ganache() -> Generator[GanacheInstance, None, None]:
"""Fixture that runs ganache"""
if not shutil.which("ganache"):
raise Exception(
"ganache was not found in PATH, you can install it with `npm install -g ganache`"
)
# Address #1 when ganache is run with `--wallet.seed test`, it starts with 1000 ETH
eth_address = "0xae17D2dD99e07CA3bF2571CCAcEAA9e2Aefc2Dc6"
eth_privkey = "0xe48ba530a63326818e116be262fd39ae6dcddd89da4b1f578be8afd4e8894b8d"
eth = int(1e18 * 1e6)
port = 8545
with subprocess.Popen(
f"""ganache
--port {port}
--chain.networkId 1
--chain.chainId 1
--account {eth_privkey},{eth}
""".replace(
"\n", " "
),
shell=True,
) as p:
sleep(3)
yield GanacheInstance(f"http://127.0.0.1:{port}", eth_address, eth_privkey)
p.kill()
p.wait()
@pytest.fixture(scope="module", name="web3")
def fixture_web3(ganache: GanacheInstance):
w3 = Web3(Web3.HTTPProvider(ganache.provider, request_kwargs={"timeout": 30}))
return w3

@ -1,5 +1,6 @@
pragma solidity 0.8.10;
// overwrite abi and bin:
// solc tests/storage-layout/storage_layout-0.8.10.sol --abi --bin -o tests/storage-layout --overwrite
// solc StorageLayout.sol --abi --bin --overwrite
contract StorageLayout {
uint248 packedUint = 1;
bool packedBool = true;

@ -0,0 +1,56 @@
{
"masterCopy": {
"name": "masterCopy",
"type_string": "address",
"slot": 0,
"size": 160,
"offset": 0,
"value": "0x0000000000000000000000000000000000000000",
"elems": {}
},
"ADMIN_SLOT": {
"name": "ADMIN_SLOT",
"type_string": "address",
"slot": 7616251639890160809447714111544359812065171195189364993079081710756264753419,
"size": 160,
"offset": 0,
"value": "0xae17D2dD99e07CA3bF2571CCAcEAA9e2Aefc2Dc6",
"elems": {}
},
"IMPLEMENTATION_SLOT": {
"name": "IMPLEMENTATION_SLOT",
"type_string": "address",
"slot": 24440054405305269366569402256811496959409073762505157381672968839269610695612,
"size": 160,
"offset": 0,
"value": "0x54006763154c764da4AF42a8c3cfc25Ea29765D5",
"elems": {}
},
"ROLLBACK_SLOT": {
"name": "ROLLBACK_SLOT",
"type_string": "bool",
"slot": 33048860383849004559742813297059419343339852917517107368639918720169455489347,
"size": 1,
"offset": 0,
"value": true,
"elems": {}
},
"BEACON_SLOT": {
"name": "BEACON_SLOT",
"type_string": "address",
"slot": 74152234768234802001998023604048924213078445070507226371336425913862612794704,
"size": 160,
"offset": 0,
"value": "0x54006763154c764da4AF42a8c3cfc25Ea29765D5",
"elems": {}
},
"fallback_sload_hardcoded": {
"name": "fallback_sload_hardcoded",
"type_string": "address",
"slot": 89532207833283453166981358064394884954800891875771469636219037672473505217783,
"size": 160,
"offset": 0,
"value": "0x54006763154c764da4AF42a8c3cfc25Ea29765D5",
"elems": {}
}
}

@ -0,0 +1 @@
[{"stateMutability":"nonpayable","type":"fallback"},{"inputs":[],"name":"store","outputs":[],"stateMutability":"nonpayable","type":"function"}]

@ -0,0 +1 @@
608060405234801561001057600080fd5b5061030b806100206000396000f3fe608060405234801561001057600080fd5b506004361061002f5760003560e01c8063975057e71461009757610030565b5b600180035473ffffffffffffffffffffffffffffffffffffffff600054167fc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7543660008037600080366000845af43d806000803e816000811461009257816000f35b816000fd5b61009f6100a1565b005b60006100ab6101a8565b9050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16146100e657600080fd5b60007f10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b9050600033905080825560007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc60001b905060007354006763154c764da4af42a8c3cfc25ea29765d59050808255807fc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf75561018460016101d6565b6101a17354006763154c764da4af42a8c3cfc25ea29765d5610220565b5050505050565b6000807f10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b9050805491505090565b806102037f4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd914360001b61025e565b60000160006101000a81548160ff02191690831515021790555050565b600060017fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d5160001c61025291906102a1565b60001b90508181555050565b6000819050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006102ac82610268565b91506102b783610268565b9250828210156102ca576102c9610272565b5b82820390509291505056fea2646970667358221220f079473c1b94744ac2818f521ccef06187a433d996633e61e51a86dfb60cc6ff64736f6c634300080a0033

@ -0,0 +1,141 @@
pragma solidity 0.8.10;
// overwrite abi and bin:
// solc UnstructuredStorageLayout.sol --abi --bin --overwrite
library StorageSlot {
struct AddressSlot {
address value;
}
struct BooleanSlot {
bool value;
}
struct Bytes32Slot {
bytes32 value;
}
struct Uint256Slot {
uint256 value;
}
/**
* @dev Returns an `AddressSlot` with member `value` located at `slot`.
*/
function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
}
/**
* @dev Returns an `BooleanSlot` with member `value` located at `slot`.
*/
function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
}
/**
* @dev Returns an `Bytes32Slot` with member `value` located at `slot`.
*/
function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
}
/**
* @dev Returns an `Uint256Slot` with member `value` located at `slot`.
*/
function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
}
}
contract UnstructuredStorageLayout {
bytes32 constant ADMIN_SLOT = keccak256("org.zeppelinos.proxy.admin");
// This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1.
bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
// This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1
bytes32 private constant ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143;
bytes32 constant BEACON_SLOT = bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1);
address internal masterCopy;
function _admin() internal view returns (address admin) {
bytes32 slot = ADMIN_SLOT;
assembly {
admin := sload(slot)
}
}
function _implementation() internal view returns (address) {
address _impl;
bytes32 slot = IMPLEMENTATION_SLOT;
assembly {
_impl := sload(slot)
}
return _impl;
}
function _set_rollback(bool _rollback) internal {
StorageSlot.getBooleanSlot(ROLLBACK_SLOT).value = _rollback;
}
function _set_beacon(address _beacon) internal {
bytes32 slot = bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1);
assembly {
sstore(slot, _beacon)
}
}
function store() external {
address admin = _admin();
require(admin == address(0));
bytes32 admin_slot = ADMIN_SLOT;
address sender = msg.sender;
assembly {
sstore(admin_slot, sender)
}
bytes32 impl_slot = IMPLEMENTATION_SLOT;
address _impl = address(0x0054006763154c764da4af42a8c3cfc25ea29765d5);
assembly {
sstore(impl_slot, _impl)
sstore(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7, _impl)
}
_set_rollback(true);
_set_beacon(address(0x0054006763154c764da4af42a8c3cfc25ea29765d5));
}
// Code position in storage is keccak256("PROXIABLE") = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7"
fallback() external {
assembly { // solium-disable-line
let nonsense := sload(sub(1,1))
let _masterCopy := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff)
let contractLogic := sload(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7)
calldatacopy(0x0, 0x0, calldatasize())
let success := delegatecall(gas(), contractLogic, 0x0, calldatasize(), 0, 0)
let retSz := returndatasize()
returndatacopy(0, 0, retSz)
switch success
case 0 {
revert(0, retSz)
}
default {
return(0, retSz)
}
}
}
}

@ -1,14 +1,9 @@
import json
import re
import shutil
import subprocess
from time import sleep
import json
from pathlib import Path
from typing import Generator
import pytest
from deepdiff import DeepDiff
from web3 import Web3
from web3.contract import Contract
from slither import Slither
@ -16,50 +11,6 @@ from slither.tools.read_storage import SlitherReadStorage, RpcInfo
TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data"
# pylint: disable=too-few-public-methods
class GanacheInstance:
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", name="web3")
def fixture_web3(ganache: GanacheInstance):
w3 = Web3(Web3.HTTPProvider(ganache.provider, request_kwargs={"timeout": 30}))
return w3
@pytest.fixture(scope="module", name="ganache")
def fixture_ganache() -> Generator[GanacheInstance, None, None]:
"""Fixture that runs ganache"""
if not shutil.which("ganache"):
raise Exception(
"ganache was not found in PATH, you can install it with `npm install -g ganache`"
)
# Address #1 when ganache is run with `--wallet.seed test`, it starts with 1000 ETH
eth_address = "0xae17D2dD99e07CA3bF2571CCAcEAA9e2Aefc2Dc6"
eth_privkey = "0xe48ba530a63326818e116be262fd39ae6dcddd89da4b1f578be8afd4e8894b8d"
eth = int(1e18 * 1e6)
port = 8545
with subprocess.Popen(
f"""ganache
--port {port}
--chain.networkId 1
--chain.chainId 1
--account {eth_privkey},{eth}
""".replace(
"\n", " "
),
shell=True,
) as p:
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) -> str:
with open(file_path, "r", encoding="utf8") as f:
@ -89,24 +40,29 @@ def deploy_contract(w3, ganache, contract_bin, contract_abi) -> Contract:
# pylint: disable=too-many-locals
@pytest.mark.parametrize(
"test_contract, storage_file",
[("StorageLayout", "storage_layout"), ("UnstructuredStorageLayout", "unstructured_storage")],
)
@pytest.mark.usefixtures("web3", "ganache")
def test_read_storage(web3, ganache, solc_binary_path) -> None:
def test_read_storage(test_contract, storage_file, web3, ganache, solc_binary_path) -> None:
solc_path = solc_binary_path(version="0.8.10")
assert web3.is_connected()
bin_path = Path(TEST_DATA_DIR, "StorageLayout.bin").as_posix()
abi_path = Path(TEST_DATA_DIR, "StorageLayout.abi").as_posix()
bin_path = Path(TEST_DATA_DIR, f"{test_contract}.bin").as_posix()
abi_path = Path(TEST_DATA_DIR, f"{test_contract}.abi").as_posix()
bytecode = get_source_file(bin_path)
abi = get_source_file(abi_path)
contract = deploy_contract(web3, ganache, bytecode, abi)
contract.functions.store().transact({"from": ganache.eth_address})
address = contract.address
sl = Slither(Path(TEST_DATA_DIR, "storage_layout-0.8.10.sol").as_posix(), solc=solc_path)
sl = Slither(Path(TEST_DATA_DIR, f"{test_contract}.sol").as_posix(), solc=solc_path)
contracts = sl.contracts
rpc_info: RpcInfo = RpcInfo(ganache.provider)
srs = SlitherReadStorage(contracts, 100, rpc_info)
srs.unstructured = True
srs.storage_address = address
srs.get_all_storage_variables()
srs.get_storage_layout()
@ -116,7 +72,7 @@ def test_read_storage(web3, ganache, solc_binary_path) -> None:
slot_infos_json = srs.to_json()
json.dump(slot_infos_json, file, indent=4)
expected_file = Path(TEST_DATA_DIR, "TEST_storage_layout.json").as_posix()
expected_file = Path(TEST_DATA_DIR, f"TEST_{storage_file}.json").as_posix()
with open(expected_file, "r", encoding="utf8") as f:
expected = json.load(f)

@ -21,39 +21,40 @@ def test_constant_folding_rational(solc_binary_path):
variable_a = contract.get_state_variable_from_name("a")
assert str(variable_a.type) == "uint256"
assert str(ConstantFolding(variable_a.expression, "uint256").result()) == "10"
assert ConstantFolding(variable_a.expression, "uint256").result().value == 10
variable_b = contract.get_state_variable_from_name("b")
assert str(variable_b.type) == "int128"
assert str(ConstantFolding(variable_b.expression, "int128").result()) == "2"
assert ConstantFolding(variable_b.expression, "int128").result().value == 2
variable_c = contract.get_state_variable_from_name("c")
assert str(variable_c.type) == "int64"
assert str(ConstantFolding(variable_c.expression, "int64").result()) == "3"
assert ConstantFolding(variable_c.expression, "int64").result().value == 3
variable_d = contract.get_state_variable_from_name("d")
assert str(variable_d.type) == "int256"
assert str(ConstantFolding(variable_d.expression, "int256").result()) == "1500"
assert ConstantFolding(variable_d.expression, "int256").result().value == 1500
variable_e = contract.get_state_variable_from_name("e")
assert str(variable_e.type) == "uint256"
assert (
str(ConstantFolding(variable_e.expression, "uint256").result())
== "57896044618658097711785492504343953926634992332820282019728792003956564819968"
ConstantFolding(variable_e.expression, "uint256").result().value
== 57896044618658097711785492504343953926634992332820282019728792003956564819968
)
variable_f = contract.get_state_variable_from_name("f")
assert str(variable_f.type) == "uint256"
assert (
str(ConstantFolding(variable_f.expression, "uint256").result())
== "115792089237316195423570985008687907853269984665640564039457584007913129639935"
ConstantFolding(variable_f.expression, "uint256").result().value
== 115792089237316195423570985008687907853269984665640564039457584007913129639935
)
variable_g = contract.get_state_variable_from_name("g")
assert str(variable_g.type) == "int64"
assert str(ConstantFolding(variable_g.expression, "int64").result()) == "-7"
assert ConstantFolding(variable_g.expression, "int64").result().value == -7
# pylint: disable=too-many-locals
def test_constant_folding_binary_expressions(solc_binary_path):
sl = Slither(
Path(CONSTANT_FOLDING_TEST_ROOT, "constant_folding_binop.sol").as_posix(),
@ -63,51 +64,65 @@ def test_constant_folding_binary_expressions(solc_binary_path):
variable_a = contract.get_state_variable_from_name("a")
assert str(variable_a.type) == "uint256"
assert str(ConstantFolding(variable_a.expression, "uint256").result()) == "0"
assert ConstantFolding(variable_a.expression, "uint256").result().value == 0
variable_b = contract.get_state_variable_from_name("b")
assert str(variable_b.type) == "uint256"
assert str(ConstantFolding(variable_b.expression, "uint256").result()) == "3"
assert ConstantFolding(variable_b.expression, "uint256").result().value == 3
variable_c = contract.get_state_variable_from_name("c")
assert str(variable_c.type) == "uint256"
assert str(ConstantFolding(variable_c.expression, "uint256").result()) == "3"
assert ConstantFolding(variable_c.expression, "uint256").result().value == 3
variable_d = contract.get_state_variable_from_name("d")
assert str(variable_d.type) == "bool"
assert str(ConstantFolding(variable_d.expression, "bool").result()) == "False"
assert ConstantFolding(variable_d.expression, "bool").result().value is False
variable_e = contract.get_state_variable_from_name("e")
assert str(variable_e.type) == "bool"
assert str(ConstantFolding(variable_e.expression, "bool").result()) == "False"
assert ConstantFolding(variable_e.expression, "bool").result().value is False
variable_f = contract.get_state_variable_from_name("f")
assert str(variable_f.type) == "bool"
assert str(ConstantFolding(variable_f.expression, "bool").result()) == "True"
assert ConstantFolding(variable_f.expression, "bool").result().value is True
variable_g = contract.get_state_variable_from_name("g")
assert str(variable_g.type) == "bool"
assert str(ConstantFolding(variable_g.expression, "bool").result()) == "False"
assert ConstantFolding(variable_g.expression, "bool").result().value is False
variable_h = contract.get_state_variable_from_name("h")
assert str(variable_h.type) == "bool"
assert str(ConstantFolding(variable_h.expression, "bool").result()) == "False"
assert ConstantFolding(variable_h.expression, "bool").result().value is False
variable_i = contract.get_state_variable_from_name("i")
assert str(variable_i.type) == "bool"
assert str(ConstantFolding(variable_i.expression, "bool").result()) == "True"
assert ConstantFolding(variable_i.expression, "bool").result().value is True
variable_j = contract.get_state_variable_from_name("j")
assert str(variable_j.type) == "bool"
assert str(ConstantFolding(variable_j.expression, "bool").result()) == "False"
assert ConstantFolding(variable_j.expression, "bool").result().value is False
variable_k = contract.get_state_variable_from_name("k")
assert str(variable_k.type) == "bool"
assert str(ConstantFolding(variable_k.expression, "bool").result()) == "True"
assert ConstantFolding(variable_k.expression, "bool").result().value is True
variable_l = contract.get_state_variable_from_name("l")
assert str(variable_l.type) == "uint256"
assert (
str(ConstantFolding(variable_l.expression, "uint256").result())
== "115792089237316195423570985008687907853269984665640564039457584007913129639935"
ConstantFolding(variable_l.expression, "uint256").result().value
== 115792089237316195423570985008687907853269984665640564039457584007913129639935
)
IMPLEMENTATION_SLOT = contract.get_state_variable_from_name("IMPLEMENTATION_SLOT")
assert str(IMPLEMENTATION_SLOT.type) == "bytes32"
assert (
int.from_bytes(
ConstantFolding(IMPLEMENTATION_SLOT.expression, "bytes32").result().value,
byteorder="big",
)
== 24440054405305269366569402256811496959409073762505157381672968839269610695612
)
variable_m = contract.get_state_variable_from_name("m")
assert str(variable_m.type) == "bytes2"
assert ConstantFolding(variable_m.expression, "bytes2").result().value == "ab"

@ -11,4 +11,6 @@ contract BinOp {
bool j = true && false;
bool k = true || false;
uint l = uint(1) - uint(2);
bytes32 IMPLEMENTATION_SLOT = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1);
bytes2 m = "ab";
}
Loading…
Cancel
Save