mirror of https://github.com/crytic/slither
commit
fc574d2c04
@ -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 |
@ -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 |
@ -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…
Reference in new issue