Merge branch 'crytic:dev' into dev

pull/1022/head
Tadashi 3 years ago committed by GitHub
commit fc574d2c04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/ISSUE_TEMPLATE/bug_report.yml
  2. 4
      .github/workflows/IR.yml
  3. 2
      .github/workflows/ci.yml
  4. 4
      .github/workflows/detectors.yml
  5. 5
      .github/workflows/features.yml
  6. 4
      .github/workflows/parser.yml
  7. 53
      .github/workflows/read_storage.yml
  8. 7
      setup.py
  9. 2
      slither/core/compilation_unit.py
  10. 2
      slither/core/declarations/function.py
  11. 10
      slither/core/solidity_types/array_type.py
  12. 10
      slither/core/solidity_types/elementary_type.py
  13. 1
      slither/printers/all_printers.py
  14. 61
      slither/printers/summary/when_not_paused.py
  15. 91
      slither/tools/read_storage/README.md
  16. 1
      slither/tools/read_storage/__init__.py
  17. 145
      slither/tools/read_storage/__main__.py
  18. 551
      slither/tools/read_storage/read_storage.py
  19. 11
      slither/tools/read_storage/utils/__init__.py
  20. 100
      slither/tools/read_storage/utils/utils.py
  21. 71
      slither/utils/expression_manipulations.py
  22. 24
      tests/slithir/ternary_expressions.sol
  23. 38
      tests/slithir/test_ternary_expressions.py
  24. 1
      tests/storage-layout/StorageLayout.abi
  25. 1
      tests/storage-layout/StorageLayout.bin
  26. 469
      tests/storage-layout/TEST_storage_layout.json
  27. 74
      tests/storage-layout/storage_layout-0.8.10.sol
  28. 139
      tests/test_read_storage.py
  29. 39
      tests/test_storage_layout.py

@ -37,7 +37,7 @@ body:
description: |
Please copy and paste any relevant log output. This
will be automatically formatted into code, so no need for backticks.
render: shell
render: shell
label: "Relevant log output:"
id: logs
type: textarea

@ -35,7 +35,9 @@ jobs:
run: |
python setup.py install
pip install deepdiff
pip install pytest
pip install pytest==7.0.1
pip install typing_extensions==4.1.1
pip install importlib_metadata==4.8.3
pip install "solc-select>=v1.0.0b1"
solc-select install all

@ -72,6 +72,8 @@ jobs:
pip install "solc-select>=v1.0.0b1"
solc-select install all
solc-select use 0.5.1
pip install typing_extensions==4.1.1
pip install importlib_metadata==4.8.3
- name: Set up nix
if: matrix.type == 'dapp'

@ -36,7 +36,9 @@ jobs:
python setup.py install
pip install deepdiff
pip install pytest
pip install pytest==7.0.1
pip install typing_extensions==4.1.1
pip install importlib_metadata==4.8.3
pip install "solc-select>=v1.0.0b1"
solc-select install all

@ -36,7 +36,9 @@ jobs:
python setup.py install
pip install deepdiff
pip install pytest
pip install pytest==7.0.1
pip install typing_extensions==4.1.1
pip install importlib_metadata==4.8.3
pip install "solc-select>=v1.0.0b1"
solc-select install all
@ -50,3 +52,4 @@ jobs:
run: |
pytest tests/test_features.py
pytest tests/test_constant_folding_unary.py
pytest tests/slithir/test_ternary_expressions.py

@ -36,7 +36,9 @@ jobs:
python setup.py install
pip install deepdiff
pip install pytest
pip install pytest==7.0.1
pip install typing_extensions==4.1.1
pip install importlib_metadata==4.8.3
pip install "solc-select>=v1.0.0b1"
- name: Install solc

@ -0,0 +1,53 @@
---
name: Test slither-read-storage
defaults:
run:
# To load bashrc
shell: bash -ieo pipefail {0}
on:
pull_request:
branches: [master, dev]
schedule:
# run CI every day even if no PRs/merges occur
- cron: '0 12 * * *'
jobs:
build:
name: Test slither-read-storage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: '14'
- name: Install ganache
run: npm install --global ganache
- name: Set up Python 3.6
uses: actions/setup-python@v2
with:
python-version: 3.6
- name: Install python dependencies
run: |
python3 setup.py install
pip install web3 pytest deepdiff solc-select
pip install pytest==7.0.1
pip install typing_extensions==4.1.1
pip install importlib_metadata==4.8.3
solc-select install 0.8.1
solc-select install 0.8.10
solc-select use 0.8.1
- name: Run slither-read-storage
run: |
pytest tests/test_read_storage.py
- name: Run storage layout tests
run: |
pytest tests/test_storage_layout.py

@ -14,10 +14,10 @@ setup(
install_requires=[
"prettytable>=0.7.2",
"pysha3>=1.0.2",
# "crytic-compile>=0.2.2",
"crytic-compile",
"crytic-compile>=0.2.3",
# "crytic-compile",
],
dependency_links=["git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile"],
# dependency_links=["git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile"],
license="AGPL-3.0",
long_description=long_description,
entry_points={
@ -32,6 +32,7 @@ setup(
"slither-check-kspec = slither.tools.kspec_coverage.__main__:main",
"slither-prop = slither.tools.properties.__main__:main",
"slither-mutate = slither.tools.mutator.__main__:main",
"slither-read-storage = slither.tools.read_storage.__main__:main",
]
},
)

@ -251,7 +251,7 @@ class SlitherCompilationUnit(Context):
slot = 0
offset = 0
for var in contract.state_variables_ordered:
if var.is_constant:
if var.is_constant or var.is_immutable:
continue
size, new_slot = var.type.storage_size

@ -1724,6 +1724,6 @@ class Function(SourceMapping, metaclass=ABCMeta): # pylint: disable=too-many-pu
###################################################################################
def __str__(self):
return self._name
return self.name
# endregion

@ -34,9 +34,17 @@ class ArrayType(Type):
return self._length
@property
def lenght_value(self) -> Optional[Literal]:
def length_value(self) -> Optional[Literal]:
return self._length_value
@property
def is_fixed_array(self) -> bool:
return bool(self.length)
@property
def is_dynamic_array(self) -> bool:
return not self.is_fixed_array
@property
def storage_size(self) -> Tuple[int, bool]:
if self._length_value:

@ -127,10 +127,10 @@ Max_Byte = {k: 2 ** (8 * (i + 1)) - 1 for i, k in enumerate(Byte[2:])}
Max_Byte["bytes"] = None
Max_Byte["string"] = None
Max_Byte["byte"] = 255
Min_Byte = {k: 1 << (4 + 8 * i) for i, k in enumerate(Byte[2:])}
Min_Byte = {k: 0 for k in Byte}
Min_Byte["bytes"] = 0x0
Min_Byte["string"] = None
Min_Byte["byte"] = 0x10
Min_Byte["string"] = 0x0
Min_Byte["byte"] = 0x0
MaxValues = dict(dict(Max_Int, **Max_Uint), **Max_Byte)
MinValues = dict(dict(Min_Int, **Min_Uint), **Min_Byte)
@ -188,8 +188,8 @@ class ElementaryType(Type):
return int(8)
if t == "address":
return int(160)
if t.startswith("bytes"):
return int(t[len("bytes") :])
if t.startswith("bytes") and t != "bytes":
return int(t[len("bytes") :]) * 8
return None
@property

@ -17,3 +17,4 @@ from .summary.require_calls import RequireOrAssert
from .summary.constructor_calls import ConstructorPrinter
from .guidance.echidna import Echidna
from .summary.evm import PrinterEVM
from .summary.when_not_paused import PrinterWhenNotPaused

@ -0,0 +1,61 @@
"""
Module printing summary of the contract
"""
from slither.core.declarations import Function
from slither.core.declarations.function import SolidityFunction
from slither.printers.abstract_printer import AbstractPrinter
from slither.utils import output
from slither.utils.myprettytable import MyPrettyTable
def _use_modifier(function: Function, modifier_name: str = "whenNotPaused") -> bool:
if function.is_constructor or function.view or function.pure:
return False
for internal_call in function.all_internal_calls():
if isinstance(internal_call, SolidityFunction):
continue
if any(modifier.name == modifier_name for modifier in function.modifiers):
return True
return False
class PrinterWhenNotPaused(AbstractPrinter):
ARGUMENT = "pausable"
HELP = "Print functions that do not use whenNotPaused"
WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#when-not-paused"
def output(self, _filename: str) -> output.Output:
"""
_filename is not used
Args:
_filename(string)
"""
modifier_name: str = "whenNotPaused"
txt = ""
txt += "Constructor and pure/view functions are not displayed\n"
all_tables = []
for contract in self.slither.contracts:
txt += f"\n{contract.name}:\n"
table = MyPrettyTable(["Name", "Use whenNotPaused"])
for function in contract.functions_entry_points:
status = "X" if _use_modifier(function, modifier_name) else ""
table.add_row([function.solidity_signature, status])
txt += str(table) + "\n"
all_tables.append((contract.name, table))
self.info(txt)
res = self.generate_output(txt)
for name, table in all_tables:
res.add_pretty_table(table, name)
return res

@ -0,0 +1,91 @@
# Slither-read-storage
Slither-read-storage is a tool to retrieve the storage slots and values of entire contracts or single variables.
## Usage
### CLI Interface
```shell
positional arguments:
contract_source (DIR) ADDRESS The deployed contract address if verified on etherscan. Prepend project directory for unverified contracts.
optional arguments:
--variable-name VARIABLE_NAME The name of the variable whose value will be returned.
--rpc-url RPC_URL An endpoint for web3 requests.
--key KEY The key/ index whose value will be returned from a mapping or array.
--deep-key DEEP_KEY The key/ index whose value will be returned from a deep mapping or multidimensional array.
--struct-var STRUCT_VAR The name of the variable whose value will be returned from a struct.
--storage-address STORAGE_ADDRESS The address of the storage contract (if a proxy pattern is used).
--contract-name CONTRACT_NAME The name of the logic contract.
--layout Toggle used to write a JSON file with the entire storage layout.
--value Toggle used to include values in output.
--max-depth MAX_DEPTH Max depth to search in data structure.
```
### Examples
Retrieve the storage slots of a local contract:
```shell
slither-read-storage file.sol 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 --layout
```
Retrieve the storage slots of a contract verified on an Etherscan-like platform:
```shell
slither-read-storage 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 --layout
```
To retrieve the values as well, pass `--value` and `--rpc-url $RPC_URL`:
```shell
slither-read-storage 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 --layout --rpc-url $RPC_URL --value
```
To view only the slot of the `slot0` structure variable, pass `--variable-name slot0`:
```shell
slither-read-storage 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 --variable-name slot0 --rpc-url https://mainnet.infura.io/v3/04942f7970ef41cc847a147bc64e460e --value
```
To view a member of the `slot0` struct, pass `--struct-var tick`
```shell
slither-read-storage 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 --variable-name slot0 --rpc-url https://mainnet.infura.io/v3/04942f7970ef41cc847a147bc64e460e --value --struct-var tick
```
Retrieve the ERC20 balance slot of an account:
```shell
slither-read-storage 0xa2327a938Febf5FEC13baCFb16Ae10EcBc4cbDCF --variable-name balances --key 0xab5801a7d398351b8be11c439e05c5b3259aec9b
```
To retrieve the actual balance, pass `--variable-name balances` and `--key 0xab5801a7d398351b8be11c439e05c5b3259aec9b`. (`balances` is a `mapping(address => uint)`)
Since this contract uses the delegatecall-proxy pattern, the proxy address must be passed as the `--storage-address`. Otherwise, it is not required.
```shell
slither-read-storage 0xa2327a938Febf5FEC13baCFb16Ae10EcBc4cbDCF --variable-name balances --key 0xab5801a7d398351b8be11c439e05c5b3259aec9b --storage-address 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 --rpc-url $RPC_URL --value
```
## Troubleshooting/FAQ
- If the storage slots or values of a contract verified Etherscan are wrong, try passing `--contract $CONTRACT_NAME` explicitly. Otherwise, the storage may be retrieved from storage slots based off an unrelated contract (Etherscan includes these). (Also, make sure that the RPC is for the correct network.)
- If Etherscan fails to return a source code, try passing `--etherscan-apikey $API_KEY` to avoid hitting a rate-limit.
- How do I use this tool on other chains?
If an EVM chain has an Etherscan-like platform the crytic-compile supports, this tool supports it by making the following modifications.
Take Avalanche, for instance:
```shell
slither-read-storage avax:0x0000000000000000000000000000000000000000 --layout --value --rpc-url $AVAX_RPC_URL
```
## Limitations
- Requires source code.
- Only works on Solidity contracts.
- Cannot find variables with unstructured storage.
- Does not support all data types (please open an issue or PR).
- Mappings cannot be completely enumerated since all keys used historically are not immediately available.

@ -0,0 +1 @@
from .read_storage import SlitherReadStorage

@ -0,0 +1,145 @@
"""
Tool to read on-chain storage from EVM
"""
import json
import argparse
from crytic_compile import cryticparser
from slither import Slither
from slither.tools.read_storage.read_storage import SlitherReadStorage
def parse_args() -> argparse.Namespace:
"""Parse the underlying arguments for the program.
Returns:
The arguments for the program.
"""
parser = argparse.ArgumentParser(
description="Read a variable's value from storage for a deployed contract",
usage=(
"\nTo retrieve a single variable's value:\n"
+ "\tslither-read-storage $TARGET address --variable-name $NAME\n"
+ "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"
+ "TARGET can be a contract address or project directory"
),
)
parser.add_argument(
"contract_source",
help="The deployed contract address if verified on etherscan. Prepend project directory for unverified contracts.",
nargs="+",
)
parser.add_argument(
"--variable-name",
help="The name of the variable whose value will be returned.",
default=None,
)
parser.add_argument("--rpc-url", help="An endpoint for web3 requests.")
parser.add_argument(
"--key",
help="The key/ index whose value will be returned from a mapping or array.",
default=None,
)
parser.add_argument(
"--deep-key",
help="The key/ index whose value will be returned from a deep mapping or multidimensional array.",
default=None,
)
parser.add_argument(
"--struct-var",
help="The name of the variable whose value will be returned from a struct.",
default=None,
)
parser.add_argument(
"--storage-address",
help="The address of the storage contract (if a proxy pattern is used).",
default=None,
)
parser.add_argument(
"--contract-name",
help="The name of the logic contract.",
default=None,
)
parser.add_argument(
"--layout",
action="store_true",
help="Toggle used to write a JSON file with the entire storage layout.",
)
parser.add_argument(
"--value",
action="store_true",
help="Toggle used to include values in output.",
)
parser.add_argument("--max-depth", help="Max depth to search in data structure.", default=20)
cryticparser.init(parser)
return parser.parse_args()
def main() -> None:
args = parse_args()
if len(args.contract_source) == 2:
# Source code is file.sol or project directory
source_code, target = args.contract_source
slither = Slither(source_code, **vars(args))
else:
# Source code is published and retrieved via etherscan
target = args.contract_source[0]
slither = Slither(target, **vars(args))
if args.contract_name:
contracts = slither.get_contract_from_name(args.contract_name)
else:
contracts = slither.contracts
srs = SlitherReadStorage(contracts, args.max_depth)
if args.rpc_url:
# Remove target prefix e.g. rinkeby:0x0 -> 0x0.
address = target[target.find(":") + 1 :]
# Default to implementation address unless a storage address is given.
if not args.storage_address:
args.storage_address = address
srs.storage_address = args.storage_address
srs.rpc = args.rpc_url
if args.layout:
srs.get_all_storage_variables()
srs.get_storage_layout()
else:
assert 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))
# To retrieve slot values an rpc url is required.
if args.value:
assert args.rpc_url
srs.get_slot_values()
# 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 __name__ == "__main__":
main()

@ -0,0 +1,551 @@
import sys
import logging
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
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,
)
except ImportError:
print("ERROR: in order to use slither-read-storage, you need to install web3")
print("$ pip3 install web3 --user\n")
sys.exit(-1)
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.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)
class SlotInfo(TypedDict):
type_string: str
slot: int
size: int
offset: int
value: Optional[Union[int, bool, str, ChecksumAddress, hex]]
elems: Optional[TypedDict] # same types as SlotInfo
class SlitherReadStorageException(Exception):
pass
# pylint: disable=too-many-instance-attributes
class SlitherReadStorage:
def __init__(self, contracts, max_depth):
self._contracts: List[Contract] = contracts
self._max_depth: int = max_depth
self._log: str = ""
self._slot_info: SlotInfo = {}
self._target_variables = []
self._web3: Optional[Web3] = None
self._checksum_address: Optional[ChecksumAddress] = None
self.storage_address: Optional[str] = None
self.rpc: Optional[str] = None
@property
def contracts(self) -> List[Contract]:
return self._contracts
@property
def max_depth(self) -> int:
return int(self._max_depth)
@property
def log(self) -> str:
return self._log
@log.setter
def log(self, log) -> str:
self._log = log
@property
def web3(self) -> Web3:
if not self._web3:
self._web3 = Web3(Web3.HTTPProvider(self.rpc))
return self._web3
@property
def checksum_address(self) -> ChecksumAddress:
if not self._checksum_address:
self._checksum_address = self.web3.toChecksumAddress(self.storage_address)
return self._checksum_address
@property
def target_variables(self) -> List[Tuple[Contract, StateVariable]]:
"""Storage variables (not constant or immutable) and their associated contract."""
return self._target_variables
@property
def slot_info(self) -> 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 = {}
for contract, var in self.target_variables:
type_ = var.type
info = self.get_storage_slot(var, contract)
tmp[var.name] = info
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
self._slot_info = tmp
def get_storage_slot(
self,
target_variable: StateVariable,
contract: Contract,
**kwargs,
) -> 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.
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 = ""
var_log_name = target_variable.name
try:
int_slot, size, offset, type_to = self.get_variable_info(contract, target_variable)
except KeyError:
# Only the child contract of a parent contract will show up in the storage layout when inheritance is used
logger.info(
f"\nContract {contract} not found in storage layout. It is possibly a parent contract\n"
)
return None
slot = int.to_bytes(int_slot, 32, byteorder="big")
if is_elementary(target_variable.type):
type_to = target_variable.type.name
elif is_array(target_variable.type) and key:
info, type_to, slot, size, offset = self._find_array_slot(
target_variable, 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
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:
info, type_to, slot, size, offset = self._find_mapping_slot(
target_variable, slot, key, struct_var=struct_var, deep_key=deep_key
)
self.log += info
int_slot = int.from_bytes(slot, byteorder="big")
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,
}
def get_target_variables(self, **kwargs) -> None:
"""
Retrieves every instance of a given variable in a list of contracts.
Should be called after setting `target_variables` with `get_all_storage_variables()`.
**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.
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
)
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())
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"]
)
logger.info(f"\nValue: {v['value']}\n")
return self.slot_info
def get_all_storage_variables(self, func: Callable = None) -> 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.variables
if not var.is_constant and not var.is_immutable
],
)
)
@staticmethod
def _find_struct_var_slot(
elems: List[StructureVariable], slot: 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.
struct_var (str): The target structure variable.
Returns:
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 (in bits) of the target variable.
offset (int): The size of other variables that share the same slot.
"""
slot = int.from_bytes(slot, "big")
offset = 0
for var in elems:
size = var.type.size
if offset >= 256:
slot += 1
offset = 0
if struct_var == var.name:
type_to = var.type.name
break # found struct var
offset += size
slot = int.to_bytes(slot, 32, byteorder="big")
info = f"\nStruct Variable: {struct_var}"
return info, type_to, slot, size, offset
# pylint: disable=too-many-branches
@staticmethod
def _find_array_slot(
target_variable: StateVariable,
slot: bytes,
key: int,
deep_key: int = None,
struct_var: str = None,
) -> Tuple[str, str, bytes]:
"""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.
key (int): The target variable's index position.
deep_key (int, optional): Secondary index if nested array.
struct_var (str, optional): Structure variable name.
Returns:
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.
"""
info = f"\nKey: {key}"
offset = 0
size = 256
if is_array(
target_variable.type.type
): # multidimensional array uint[i][], , uint[][i], or uint[][]
size = target_variable.type.type.type.size
type_to = target_variable.type.type.type.name
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))
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[][]
# 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")
# 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:
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 not struct_var:
return info, type_to, int.to_bytes(slot_int, 32, "big"), size, offset
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
)
info += info_tmp
else:
type_to = target_variable.type.type.name
size = target_variable.type.type.size # bits
elif is_user_defined_type(target_variable.type.type): # struct[]
slot = keccak(slot)
slot_int = int.from_bytes(slot, "big") + int(key)
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
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
)
info += info_tmp
else:
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
slot = int.to_bytes(slot_int, 32, byteorder="big")
return info, type_to, slot, size, offset
@staticmethod
def _find_mapping_slot(
target_variable: StateVariable,
slot: bytes,
key: Union[int, str],
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.
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.
deep_key (int, optional): Key of a mapping embedded within another mapping.
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.
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.
"""
info = ""
offset = 0
if key:
info += f"\nKey: {key}"
if deep_key:
info += f"\nDeep Key: {deep_key}"
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
): # mapping(elem => struct)
assert struct_var
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 => ???))
assert deep_key
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)
# If deep map, will be keccak256(abi.encode(key1, keccak256(abi.encode(key0, uint(slot)))))
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
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
): # mapping(elem => mapping(elem => struct))
assert struct_var
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
)
info += info_tmp
# 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
size = byte_size * 8 # bits
return info, type_to, slot, size, offset
@staticmethod
def get_variable_info(
contract: Contract, target_variable: StateVariable
) -> Tuple[int, int, int, str]:
"""Return slot, size, offset, and type."""
type_to = str(target_variable.type)
byte_size, _ = target_variable.type.storage_size
size = byte_size * 8 # bits
(int_slot, offset) = contract.compilation_unit.storage_layout_of(contract, target_variable)
offset *= 8 # bits
logger.info(
f"\nContract '{contract.name}'\n{target_variable.canonical_name} with type {target_variable.type} is located at slot: {int_slot}\n"
)
return int_slot, size, offset, type_to
@staticmethod
def convert_value_to_type(
hex_bytes: HexBytes, size: int, offset: int, type_to: str
) -> Union[int, bool, str, ChecksumAddress, hex]:
"""Convert slot data to type representation."""
# Account for storage packing
offset_hex_bytes = get_offset_value(hex_bytes, offset, size)
try:
value = coerce_type(type_to, offset_hex_bytes)
except ValueError:
return coerce_type("int", offset_hex_bytes)
return value
def _all_struct_slots(
self, var: Union[StructureVariable, StructureContract], contract: Contract, key=None
) -> Dict[str, SlotInfo]:
"""Retrieves all members of a struct."""
if isinstance(var.type.type, StructureContract):
struct_elems = var.type.type.elems_ordered
else:
struct_elems = var.type.type.type.elems_ordered
data = {}
for elem in struct_elems:
info = self.get_storage_slot(
var,
contract,
key=key,
struct_var=elem.name,
)
data[elem.name] = info
return data
def _all_array_slots(
self, var: ArrayType, contract: Contract, type_: Type, slot: int
) -> Dict[int, SlotInfo]:
"""Retrieves all members of an array."""
array_length = self._get_array_length(type_, slot)
elems = {}
if is_user_defined_type(type_.type):
for i in range(min(array_length, self.max_depth)):
elems[i] = self._all_struct_slots(var, contract, key=str(i))
continue
else:
for i in range(min(array_length, self.max_depth)):
info = self.get_storage_slot(
var,
contract,
key=str(i),
)
elems[i] = info
if is_array(type_.type): # multidimensional array
array_length = self._get_array_length(type_.type, info["slot"])
elems[i]["elems"] = {}
for j in range(min(array_length, self.max_depth)):
info = self.get_storage_slot(
var,
contract,
key=str(i),
deep_key=str(j),
)
elems[i]["elems"][j] = info
return elems
def _get_array_length(self, type_: Type, slot: int = None) -> 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.
Returns:
(int): The length of the array.
"""
val = 0
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_):
if type_.is_fixed_array:
val = int(str(type_.length))
return val

@ -0,0 +1,11 @@
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,
)

@ -0,0 +1,100 @@
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:
"""
Trims slot data to only contain the target variable's.
Args:
hex_bytes (HexBytes): String representation of type
offset (int): The size (in bits) of other variables that share the same slot.
size (int): The size (in bits) of the target variable.
Returns:
(bytes): The target variable's trimmed data.
"""
size = int(size / 8)
offset = int(offset / 8)
if offset == 0:
value = hex_bytes[-size:]
else:
start = size + offset
value = hex_bytes[-start:-offset]
return value
def coerce_type(solidity_type: str, value: bytes) -> Union[int, bool, str, ChecksumAddress, hex]:
"""
Converts input to the indicated type.
Args:
solidity_type (str): String representation of type.
value (bytes): The value to be converted.
Returns:
(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:
# 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])
elif "address" in solidity_type:
converted_value = to_checksum_address(value)
else:
converted_value = value.hex()
return converted_value
def get_storage_data(web3, checksum_address: ChecksumAddress, slot: bytes) -> HexBytes:
"""
Retrieves the storage data from the blockchain at target address and slot.
Args:
web3: Web3 instance provider.
checksum_address (ChecksumAddress): The address to query.
slot (bytes): The slot to retrieve data from.
Returns:
(HexBytes): The slot's storage data.
"""
return bytes(web3.eth.get_storage_at(checksum_address, slot)).rjust(
32, bytes(1)
) # pad to 32 bytes

@ -3,12 +3,13 @@
as they should be immutable
"""
import copy
from typing import Union, Callable
from slither.core.expressions import UnaryOperation
from slither.core.expressions.assignment_operation import AssignmentOperation
from slither.core.expressions.binary_operation import BinaryOperation
from slither.core.expressions.call_expression import CallExpression
from slither.core.expressions.conditional_expression import ConditionalExpression
from slither.core.expressions.expression import Expression
from slither.core.expressions.identifier import Identifier
from slither.core.expressions.index_access import IndexAccess
from slither.core.expressions.literal import Literal
@ -20,7 +21,9 @@ from slither.core.expressions.type_conversion import TypeConversion
from slither.all_exceptions import SlitherException
# pylint: disable=protected-access
def f_expressions(e, x):
def f_expressions(
e: AssignmentOperation, x: Union[Identifier, Literal, MemberAccess, IndexAccess]
) -> None:
e._expressions.append(x)
@ -37,7 +40,7 @@ def f_called(e, x):
class SplitTernaryExpression:
def __init__(self, expression):
def __init__(self, expression: Union[AssignmentOperation, ConditionalExpression]) -> None:
if isinstance(expression, ConditionalExpression):
self.true_expression = copy.copy(expression.then_expression)
@ -49,7 +52,13 @@ class SplitTernaryExpression:
self.condition = None
self.copy_expression(expression, self.true_expression, self.false_expression)
def apply_copy(self, next_expr, true_expression, false_expression, f):
def apply_copy(
self,
next_expr: Expression,
true_expression: Union[AssignmentOperation, MemberAccess],
false_expression: Union[AssignmentOperation, MemberAccess],
f: Callable,
) -> bool:
if isinstance(next_expr, ConditionalExpression):
f(true_expression, copy.copy(next_expr.then_expression))
@ -61,9 +70,10 @@ class SplitTernaryExpression:
f(false_expression, copy.copy(next_expr))
return True
# pylint: disable=too-many-branches
def copy_expression(
self, expression, true_expression, false_expression
): # pylint: disable=too-many-branches
self, expression: Expression, true_expression: Expression, false_expression: Expression
) -> None:
if self.condition:
return
@ -87,6 +97,12 @@ class SplitTernaryExpression:
false_expression._expressions = []
for next_expr in expression.expressions:
if isinstance(next_expr, IndexAccess):
# create an index access for each branch
if isinstance(next_expr.expression_right, ConditionalExpression):
next_expr = _handle_ternary_access(
next_expr, true_expression, false_expression
)
if self.apply_copy(next_expr, true_expression, false_expression, f_expressions):
# always on last arguments added
self.copy_expression(
@ -115,16 +131,7 @@ class SplitTernaryExpression:
false_expression.arguments[-1],
)
elif isinstance(expression, TypeConversion):
next_expr = expression.expression
if self.apply_copy(next_expr, true_expression, false_expression, f_expression):
self.copy_expression(
expression.expression,
true_expression.expression,
false_expression.expression,
)
elif isinstance(expression, UnaryOperation):
elif isinstance(expression, (TypeConversion, UnaryOperation)):
next_expr = expression.expression
if self.apply_copy(next_expr, true_expression, false_expression, f_expression):
self.copy_expression(
@ -137,3 +144,35 @@ class SplitTernaryExpression:
raise SlitherException(
f"Ternary operation not handled {expression}({type(expression)})"
)
def _handle_ternary_access(
next_expr: IndexAccess,
true_expression: AssignmentOperation,
false_expression: AssignmentOperation,
):
"""
Conditional ternary accesses are split into two accesses, one true and one false
E.g. x[if cond ? 1 : 2] -> if cond { x[1] } else { x[2] }
"""
true_index_access = IndexAccess(
next_expr.expression_left,
next_expr.expression_right.then_expression,
next_expr.type,
)
false_index_access = IndexAccess(
next_expr.expression_left,
next_expr.expression_right.else_expression,
next_expr.type,
)
f_expressions(
true_expression,
true_index_access,
)
f_expressions(
false_expression,
false_index_access,
)
return next_expr.expression_right

@ -0,0 +1,24 @@
contract C {
// TODO
// 1) support variable declarations
//uint min = 1 > 0 ? 1 : 2;
// 2) suppory ternary index range access
// function e(bool cond, bytes calldata x) external {
// bytes memory a = x[cond ? 1 : 2 :];
// }
function a(uint a, uint b) external {
(uint min, uint max) = a < b ? (a, b) : (b, a);
}
function b( address a, address b) external {
(address tokenA, address tokenB) = a < b ? (a, b) : (b, a);
}
bytes char;
function c(bytes memory strAddress, uint i, uint padding, uint length) external {
char[0] = strAddress[i < padding + 2 ? i : 42 + i - length];
}
function d(bool cond, bytes calldata x) external {
bytes1 a = x[cond ? 1 : 2];
}
}

@ -0,0 +1,38 @@
from slither import Slither
from slither.core.cfg.node import NodeType
from slither.slithir.operations import Assignment
from slither.core.expressions import AssignmentOperation, TupleExpression
# pylint: disable=too-many-nested-blocks
def test_ternary_conversions() -> None:
"""This tests that true and false sons define the same number of variables that the father node declares"""
slither = Slither("./tests/slithir/ternary_expressions.sol")
for contract in slither.contracts:
for function in contract.functions:
for node in function.nodes:
if node.type in [NodeType.IF, NodeType.IFLOOP]:
vars_declared = 0
vars_assigned = 0
# Iterate over true and false son
for inner_node in node.sons:
# Count all variables declared
expression = inner_node.expression
if isinstance(expression, AssignmentOperation):
var_expr = expression.expression_left
# Only tuples declare more than one var
if isinstance(var_expr, TupleExpression):
vars_declared += len(var_expr.expressions)
else:
vars_declared += 1
for ir in inner_node.irs:
# Count all variables defined
if isinstance(ir, Assignment):
vars_assigned += 1
assert vars_declared == vars_assigned
if __name__ == "__main__":
test_ternary_conversions()

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

File diff suppressed because one or more lines are too long

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

@ -0,0 +1,74 @@
// overwrite abi and bin:
// solc tests/storage-layout/storage_layout-0.8.10.sol --abi --bin -o tests/storage-layout --overwrite
contract StorageLayout {
uint248 packedUint = 1;
bool packedBool = true;
struct PackedStruct {
bool b;
uint248 a;
}
PackedStruct _packedStruct = PackedStruct(packedBool, packedUint);
mapping (uint => PackedStruct) mappingPackedStruct;
mapping (address => mapping (uint => PackedStruct)) deepMappingPackedStruct;
mapping (address => mapping (uint => bool)) deepMappingElementaryTypes;
mapping (address => PackedStruct[]) mappingDynamicArrayOfStructs;
address _address;
string _string = "slither-read-storage";
uint8 packedUint8 = 8;
bytes8 packedBytes = "aaaaaaaa";
enum Enum {
a,
b,
c
}
Enum _enumA = Enum.a;
Enum _enumB = Enum.b;
Enum _enumC = Enum.c;
uint256[3] fixedArray;
uint256[3][] dynamicArrayOfFixedArrays;
uint[][3] fixedArrayofDynamicArrays;
uint[][] multidimensionalArray;
PackedStruct[] dynamicArrayOfStructs;
PackedStruct[3] fixedArrayOfStructs;
function store() external {
require(_address == address(0));
_address = msg.sender;
mappingPackedStruct[packedUint] = _packedStruct;
deepMappingPackedStruct[_address][packedUint] = _packedStruct;
deepMappingElementaryTypes[_address][1] = true;
deepMappingElementaryTypes[_address][2] = true;
fixedArray = [1, 2, 3];
dynamicArrayOfFixedArrays.push(fixedArray);
dynamicArrayOfFixedArrays.push([4, 5, 6]);
fixedArrayofDynamicArrays[0].push(7);
fixedArrayofDynamicArrays[1].push(8);
fixedArrayofDynamicArrays[1].push(9);
fixedArrayofDynamicArrays[2].push(10);
fixedArrayofDynamicArrays[2].push(11);
fixedArrayofDynamicArrays[2].push(12);
multidimensionalArray.push([13]);
multidimensionalArray.push([14, 15]);
multidimensionalArray.push([16, 17, 18]);
dynamicArrayOfStructs.push(_packedStruct);
dynamicArrayOfStructs.push(PackedStruct(false, 10));
fixedArrayOfStructs[0] = _packedStruct;
fixedArrayOfStructs[1] = PackedStruct(false, 10);
mappingDynamicArrayOfStructs[_address].push(dynamicArrayOfStructs[0]);
mappingDynamicArrayOfStructs[_address].push(dynamicArrayOfStructs[1]);
}
}

@ -0,0 +1,139 @@
import re
import os
import sys
import json
import shutil
import subprocess
from time import sleep
from typing import Generator
import pytest
from deepdiff import DeepDiff
from slither import Slither
from slither.tools.read_storage import SlitherReadStorage
try:
from web3 import Web3
except ImportError:
print("ERROR: in order to use slither-read-storage, you need to install web3")
print("$ pip3 install web3 --user\n")
sys.exit(-1)
SLITHER_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STORAGE_TEST_ROOT = os.path.join(SLITHER_ROOT, "tests", "storage-layout")
# 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):
with open(file_path, "r", encoding="utf8") as f:
source = f.read()
return source
def deploy_contract(w3, ganache, contract_bin, contract_abi):
"""Deploy contract to the local ganache network"""
signed_txn = w3.eth.account.sign_transaction(
dict(
nonce=w3.eth.get_transaction_count(ganache.eth_address),
maxFeePerGas=20000000000,
maxPriorityFeePerGas=1,
gas=15000000,
to=b"",
data="0x" + contract_bin,
chainId=1,
),
ganache.eth_privkey,
)
tx_hash = w3.eth.send_raw_transaction(signed_txn.rawTransaction)
address = w3.eth.get_transaction_receipt(tx_hash)["contractAddress"]
contract = w3.eth.contract(address, abi=contract_abi)
return contract
# pylint: disable=too-many-locals
@pytest.mark.usefixtures("web3", "ganache")
def test_read_storage(web3, ganache):
assert web3.isConnected()
bin_path = os.path.join(STORAGE_TEST_ROOT, "StorageLayout.bin")
abi_path = os.path.join(STORAGE_TEST_ROOT, "StorageLayout.abi")
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(os.path.join(STORAGE_TEST_ROOT, "storage_layout-0.8.10.sol"))
contracts = sl.contracts
srs = SlitherReadStorage(contracts, 100)
srs.rpc = ganache.provider
srs.storage_address = address
srs.get_all_storage_variables()
srs.get_storage_layout()
srs.get_slot_values()
with open("storage_layout.json", "w", encoding="utf-8") as file:
json.dump(srs.slot_info, 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")
with open(expected_file, "r", encoding="utf8") as f:
expected = json.load(f)
with open(actual_file, "r", encoding="utf8") as f:
actual = json.load(f)
diff = DeepDiff(expected, actual, ignore_order=True, verbose_level=2, view="tree")
if diff:
for change in diff.get("values_changed", []):
path_list = re.findall(r"\['(.*?)'\]", change.path())
path = "_".join(path_list)
with open(f"{path}_expected.txt", "w", encoding="utf8") as f:
f.write(change.t1)
with open(f"{path}_actual.txt", "w", encoding="utf8") as f:
f.write(change.t2)
assert not diff

@ -0,0 +1,39 @@
import json
import os
import subprocess
from subprocess import PIPE, Popen
from slither import Slither
SLITHER_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STORAGE_TEST_ROOT = os.path.join(SLITHER_ROOT, "tests", "storage-layout")
# the storage layout has not yet changed between solidity versions so we will test with one version of the compiler
def test_storage_layout():
subprocess.run(["solc-select", "use", "0.8.10"], stdout=subprocess.PIPE, check=True)
test_item = os.path.join(STORAGE_TEST_ROOT, "storage_layout-0.8.10.sol")
sl = Slither(test_item, solc_force_legacy_json=False, disallow_partial=True)
with Popen(["solc", test_item, "--storage-layout"], stdout=PIPE) as process:
for line in process.stdout: # parse solc output
if '{"storage":[{' in line.decode("utf-8"): # find the storage layout
layout = iter(json.loads(line)["storage"])
while True:
try:
for contract in sl.contracts:
curr_var = next(layout)
var_name = curr_var["label"]
sl_name = contract.variables_as_dict[var_name]
slot, offset = contract.compilation_unit.storage_layout_of(
contract, sl_name
)
assert slot == int(curr_var["slot"])
assert offset == int(curr_var["offset"])
except StopIteration:
break
except KeyError as e:
print(f"not found {e} ")
Loading…
Cancel
Save