-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathstorage_layout.py
174 lines (127 loc) · 5.12 KB
/
storage_layout.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
import json
from collections import defaultdict
from typing import Dict, List, Tuple
import requests
from ape import chain, networks
from devtools import debug
from eth_abi import decode_single, encode_single
from eth_utils import encode_hex, keccak
from evm_trace import vmtrace
from hexbytes import HexBytes
from toolz import valfilter
from typer import Typer
app = Typer()
def make_request(method: str, params: List) -> Dict:
response = chain.provider.web3.provider.make_request(method, params)
if "error" in response:
raise ValueError(response["error"]["message"])
return response["result"]
def raw_request(method: str, params: List) -> bytes:
payload = {"method": method, "params": params, "id": None, "jsonrpc": "2.0"}
response = requests.post(chain.provider.uri, json=payload)
if response.status_code != 200:
raise ValueError(response.json()["error"]["message"])
return response.content
def batch_request(calls: List[Tuple[str, List]]):
payload = [
{"method": method, "params": params, "id": None, "jsonrpc": "2.0"}
for method, params in calls
]
batch_repsonse = requests.post(chain.provider.uri, json=payload).json()
for response in batch_repsonse:
if "error" in response:
raise ValueError(response["error"]["message"])
yield response["result"]
def get_storage_keys(account):
return make_request("parity_listStorageKeys", [account, 1_000_000, None, "latest"])
def get_storage_values(account, keys):
values = batch_request([("eth_getStorageAt", [account, key, "latest"]) for key in keys])
return list(values)
def get_storage_diff(txhash: str):
state_diff = make_request("trace_replayTransaction", [txhash, ["stateDiff"]])["stateDiff"]
storage_diff = {}
for contract, diff in state_diff.items():
storage_diff[contract] = {}
for slot, change in diff["storage"].items():
for op, delta in change.items():
if op == "+":
storage_diff[contract][slot] = delta
elif op == "*":
storage_diff[contract][slot] = delta["to"]
else:
raise NotImplementedError(f"op {op} in state diff")
return valfilter(bool, storage_diff)
def to_int(value):
if isinstance(value, str):
return int(value, 16)
if isinstance(value, bytes):
return int.from_bytes(value, "big")
raise ValueError("invalid type %s", type(value))
def find_preimages(txhash: str):
response = raw_request("trace_replayTransaction", [txhash, ["vmTrace"]])
trace = vmtrace.from_rpc_response(response)
preimages = {}
for frame in vmtrace.to_trace_frames(trace):
if frame.op == "SHA3":
size, offset = [to_int(x) for x in frame.stack[-2:]]
if size != 64:
continue
preimage = HexBytes(frame.memory[offset : offset + size])
hashed = HexBytes(keccak(preimage))
key, slot = preimage[:32], preimage[32:]
preimages[hashed.hex()] = {"key": key.hex(), "slot": slot.hex()}
if frame.op == "SSTORE":
value, slot = frame.stack[-2:]
return preimages
@app.command()
def storage(contract: str):
keys = get_storage_keys(contract)
values = get_storage_values(contract, keys)
kv = dict(zip(keys, values))
debug(kv)
def int_to_bytes32(value):
return encode_hex(encode_single("uint256", value))
def unwrap_slot(slot, value, preimages, slot_lookup):
def unwrap(slot, path):
if slot in slot_lookup:
return {**slot_lookup[slot], "path": path, "value": value}
if slot in preimages:
p_slot, p_key = preimages[slot]["slot"], preimages[slot]["key"]
from_slot = unwrap(p_slot, path + [p_key])
from_key = unwrap(p_key, path + [p_slot])
return from_slot or from_key
return unwrap(slot, [])
def decode_types(item):
values = b"".join(HexBytes(i) for i in item["path"]) + HexBytes(item["value"])
decoded = decode_single(item["abi_type"], values)
out = decoded
if item["path"]:
out = decoded[-1]
for p, v in zip(item["path"], decoded):
out = {v: out}
return {item["name"]: out}
@app.command()
def layout(txhash: str):
storage_layout = {"0xda816459f1ab5631232fe5e97a05bbbb94970c95": json.load(open("layout.json"))}
slot_lookup = {
contract: {int_to_bytes32(item["pos"]): item for item in data}
for contract, data in storage_layout.items()
}
storage_diff = get_storage_diff(txhash)
preimages = find_preimages(txhash)
results = defaultdict(dict)
for contract, storage in storage_diff.items():
if contract not in slot_lookup:
continue
for slot, value in storage.items():
item = unwrap_slot(slot, value, preimages, slot_lookup[contract])
if item is None:
continue
decoded = decode_types(item)
results[contract].update(decoded)
debug(results)
def main():
with networks.ethereum.mainnet.use_default_provider():
app()
if __name__ == "__main__":
main()