Skip to content

Commit 91ebdd9

Browse files
ep0chzer0dguidoclaude
authored
feat: add support for constant and immutable variables in slither-read-storage (#2920)
* feat: add support for constant and immutable variables in slither-read-storage This adds the --include-immutable flag to slither-read-storage which enables display of constant and immutable variables alongside regular storage variables. Features: - New --include-immutable CLI flag - Constant values extracted from expressions using ConstantFolding - Immutable values retrieved via RPC getter calls for public variables - Variables displayed with (constant) or (immutable) type suffix - Slot shows -1 for non-storage variables Fixes #1614 * fix: address code review findings for immutable/constant support - Fix pre-existing bug: lambda filter used x[1].name but x is StateVariable, not tuple - Use specific exception types instead of catching all Exception - Handle None type by showing "unknown" instead of "None" - Add debug log for private/internal immutables (cannot retrieve via RPC) - Add tests for --variable-name filter with immutable/constant variables * fix: add solc version to test fixture for CI compatibility * fix: address remaining code review findings for immutable/constant support - Remove unused `contract` parameter from `_get_immutable_value` method - Remove unused `Path` import from test file - Change test assertions from `>=` to `==` for exact count validation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: ep0chzer0 <ep0chzer0@users.noreply.github.com> Co-authored-by: Dan Guido <dan@trailofbits.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f570c78 commit 91ebdd9

File tree

3 files changed

+375
-9
lines changed

3 files changed

+375
-9
lines changed

slither/tools/read_storage/__main__.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ def parse_args() -> argparse.Namespace:
112112
help="Include unstructured storage slots",
113113
)
114114

115+
parser.add_argument(
116+
"--include-immutable",
117+
action="store_true",
118+
help="Include immutable and constant variables in output",
119+
)
120+
115121
cryticparser.init(parser)
116122

117123
return parser.parse_args()
@@ -144,6 +150,7 @@ def main() -> None:
144150

145151
srs = SlitherReadStorage(contracts, args.max_depth, rpc_info)
146152
srs.unstructured = bool(args.unstructured)
153+
srs.include_immutable = bool(args.include_immutable)
147154
# Remove target prefix e.g. rinkeby:0x0 -> 0x0.
148155
address = target[target.find(":") + 1 :]
149156
# Default to implementation address unless a storage address is given.
@@ -153,8 +160,8 @@ def main() -> None:
153160

154161
if args.variable_name:
155162
# Use a lambda func to only return variables that have same name as target.
156-
# x is a tuple (`Contract`, `StateVariable`).
157-
srs.get_all_storage_variables(lambda x: bool(x[1].name == args.variable_name))
163+
# x is a StateVariable.
164+
srs.get_all_storage_variables(lambda x: bool(x.name == args.variable_name))
158165
srs.get_target_variables(**vars(args))
159166
else:
160167
srs.get_all_storage_variables()

slither/tools/read_storage/read_storage.py

Lines changed: 169 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,13 @@ def __init__(self, contracts: list[Contract], max_depth: int, rpc_info: RpcInfo
9494
self._slot_info: dict[str, SlotInfo] = {}
9595
self._target_variables: list[tuple[Contract, StateVariable]] = []
9696
self._constant_storage_slots: list[tuple[Contract, StateVariable]] = []
97+
self._immutable_variables: list[tuple[Contract, StateVariable]] = []
98+
self._constant_variables: list[tuple[Contract, StateVariable]] = []
9799
self.rpc_info: RpcInfo | None = rpc_info
98100
self.storage_address: str | None = None
99101
self.table: MyPrettyTable | None = None
100102
self.unstructured: bool = False
103+
self.include_immutable: bool = False
101104

102105
@property
103106
def contracts(self) -> list[Contract]:
@@ -130,9 +133,19 @@ def target_variables(self) -> list[tuple[Contract, StateVariable]]:
130133

131134
@property
132135
def constant_slots(self) -> list[tuple[Contract, StateVariable]]:
133-
"""Constant bytes32 variables and their associated contract."""
136+
"""Constant bytes32 variables (unstructured storage slots) and their associated contract."""
134137
return self._constant_storage_slots
135138

139+
@property
140+
def immutable_variables(self) -> list[tuple[Contract, StateVariable]]:
141+
"""Immutable variables and their associated contract."""
142+
return self._immutable_variables
143+
144+
@property
145+
def constant_variables(self) -> list[tuple[Contract, StateVariable]]:
146+
"""Constant variables and their associated contract."""
147+
return self._constant_variables
148+
136149
@property
137150
def slot_info(self) -> dict[str, SlotInfo]:
138151
"""Contains the location, type, size, offset, and value of contract slots."""
@@ -154,6 +167,8 @@ def get_storage_layout(self) -> None:
154167
tmp[var.name].elems = elems
155168
if self.unstructured:
156169
tmp.update(self.get_unstructured_layout())
170+
if self.include_immutable:
171+
tmp.update(self.get_immutable_constant_layout())
157172
self._slot_info = tmp
158173

159174
def get_unstructured_layout(self) -> dict[str, SlotInfo]:
@@ -194,6 +209,146 @@ def get_unstructured_layout(self) -> dict[str, SlotInfo]:
194209
continue
195210
return tmp
196211

212+
def get_immutable_constant_layout(self) -> dict[str, SlotInfo]:
213+
"""
214+
Retrieves the layout for immutable and constant variables.
215+
These variables don't have storage slots - their values are either:
216+
- Embedded in bytecode (immutable)
217+
- Computed at compile time (constant)
218+
219+
For public variables, values can be retrieved via RPC by calling the getter.
220+
For private/internal, we extract from the expression if possible.
221+
"""
222+
tmp: dict[str, SlotInfo] = {}
223+
224+
# Process immutable variables
225+
for contract, var in self.immutable_variables:
226+
var_name = var.name
227+
type_ = var.type
228+
type_string = str(type_) if type_ else "unknown"
229+
byte_size, _ = type_.storage_size if type_ else (32, 0)
230+
size = byte_size * 8
231+
232+
value = self._get_immutable_value(var)
233+
234+
tmp[var_name] = SlotInfo(
235+
name=var_name,
236+
type_string=f"{type_string} (immutable)",
237+
slot=-1, # No storage slot
238+
size=size,
239+
offset=0,
240+
value=value,
241+
)
242+
self.log += f"\nImmutable: {var_name}\nType: {type_string}\nValue: {value}\n"
243+
logger.info(self.log)
244+
self.log = ""
245+
246+
# Process constant variables
247+
for contract, var in self.constant_variables:
248+
var_name = var.name
249+
type_ = var.type
250+
type_string = str(type_) if type_ else "unknown"
251+
byte_size, _ = type_.storage_size if type_ else (32, 0)
252+
size = byte_size * 8
253+
254+
value = self._get_constant_value(var)
255+
256+
tmp[var_name] = SlotInfo(
257+
name=var_name,
258+
type_string=f"{type_string} (constant)",
259+
slot=-1, # No storage slot
260+
size=size,
261+
offset=0,
262+
value=value,
263+
)
264+
self.log += f"\nConstant: {var_name}\nType: {type_string}\nValue: {value}\n"
265+
logger.info(self.log)
266+
self.log = ""
267+
268+
return tmp
269+
270+
def _get_immutable_value(self, var: StateVariable) -> int | bool | str | ChecksumAddress | None:
271+
"""
272+
Retrieves the value of an immutable variable.
273+
For public immutables, calls the getter via RPC.
274+
For private/internal, returns None (would require bytecode analysis).
275+
"""
276+
# Private/internal immutables cannot be retrieved without bytecode analysis
277+
if var.visibility != "public":
278+
logger.debug(
279+
f"Cannot retrieve value for {var.visibility} immutable {var.name} "
280+
"(would require bytecode analysis)"
281+
)
282+
return None
283+
284+
# Try to get value via RPC for public variables
285+
if self.rpc_info:
286+
try:
287+
# Call the getter function
288+
func_signature = f"{var.name}()"
289+
selector = keccak(func_signature.encode())[:4]
290+
result = self.rpc_info.web3.eth.call(
291+
{"to": self.checksum_address, "data": "0x" + selector.hex()},
292+
self.rpc_info.block,
293+
)
294+
if result:
295+
type_ = var.type
296+
if isinstance(type_, ElementaryType):
297+
return self.convert_value_to_type(result, type_.size, 0, type_.name)
298+
except (ValueError, TypeError) as e:
299+
logger.warning(f"Could not retrieve immutable {var.name} via RPC: {e}")
300+
301+
return None
302+
303+
def _get_constant_value(self, var: StateVariable) -> int | bool | str | ChecksumAddress | None:
304+
"""
305+
Retrieves the value of a constant variable from its expression.
306+
"""
307+
if var.expression is None:
308+
return None
309+
310+
try:
311+
exp = var.expression
312+
type_ = var.type
313+
314+
# Try constant folding for complex expressions
315+
if isinstance(
316+
exp,
317+
(
318+
BinaryOperation,
319+
UnaryOperation,
320+
Identifier,
321+
TupleExpression,
322+
TypeConversion,
323+
CallExpression,
324+
),
325+
):
326+
type_str = str(type_) if type_ else "uint256"
327+
exp = ConstantFolding(exp, type_str).result()
328+
329+
if isinstance(exp, Literal):
330+
value = exp.value
331+
if isinstance(type_, ElementaryType):
332+
type_name = type_.name
333+
# Handle different types
334+
if "int" in type_name:
335+
return int(value) if isinstance(value, str) else value
336+
if type_name == "bool":
337+
return value.lower() == "true" if isinstance(value, str) else bool(value)
338+
if type_name == "address":
339+
return to_checksum_address(value) if value else None
340+
if "bytes" in type_name:
341+
return value if isinstance(value, str) else hex(value)
342+
return str(value)
343+
return str(value)
344+
except NotConstant:
345+
logger.debug(f"Could not fold constant {var.name}: expression is not constant")
346+
except (ValueError, TypeError, AttributeError) as e:
347+
logger.debug(f"Could not fold constant {var.name}: {e}")
348+
349+
# Return raw expression as string if we can't fold it
350+
return str(var.expression) if var.expression else None
351+
197352
def get_storage_slot(
198353
self,
199354
target_variable: StateVariable,
@@ -373,6 +528,10 @@ def get_slot_values(self, slot_info: SlotInfo) -> None:
373528
Fetches the slot value of `SlotInfo` object
374529
:param slot_info:
375530
"""
531+
# Skip immutable/constant variables (slot=-1) - their values are already set
532+
if slot_info.slot == -1:
533+
return
534+
376535
assert self.rpc_info is not None
377536
hex_bytes = get_storage_data(
378537
self.rpc_info.web3,
@@ -396,12 +555,15 @@ def get_all_storage_variables(self, func: Callable = lambda x: x) -> None:
396555
if func(var):
397556
if var.is_stored:
398557
self._target_variables.append((contract, var))
399-
elif (
400-
self.unstructured
401-
and var.is_constant
402-
and var.type == ElementaryType("bytes32")
403-
):
404-
self._constant_storage_slots.append((contract, var))
558+
elif var.is_immutable and self.include_immutable:
559+
self._immutable_variables.append((contract, var))
560+
elif var.is_constant:
561+
if self.include_immutable:
562+
# Capture all constants for display
563+
self._constant_variables.append((contract, var))
564+
if self.unstructured and var.type == ElementaryType("bytes32"):
565+
# Also add bytes32 constants to unstructured slots
566+
self._constant_storage_slots.append((contract, var))
405567
if self.unstructured:
406568
hardcoded_slot = self.find_hardcoded_slot_in_fallback(contract)
407569
if hardcoded_slot is not None:

0 commit comments

Comments
 (0)