-
Notifications
You must be signed in to change notification settings - Fork 6
/
trace.py
191 lines (141 loc) · 4.8 KB
/
trace.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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
from helpers import opcode, is_zero
'''
Two helper functions to analyse the function traces outputted by
http://eveem.org/code/0x06012c8cf97bead5deae237070f9587f8e7a266d.json
The intermediate language used is not documented anywhere yet in full,
but you should get a good understanding of it by analysing the kitties code linked above.
'''
'''
Some opcodes that may not be obvious:
'''
'''
(MASK_SHL, size, offset, shl, expression)
is the same as:
(SHL, (MASK, size, offset, expression), shl)
Where "shl" may be negative, in which case it means SHR.
For example:
(MASK_SHL, 4, 16, -8, 0xF00)
== 0x70
(MASK_SHL, 160, 0, 0, 'CALLER')
== address(caller) - that is, first 160 bytes of the call sender
(MASK_SHL, 160, 0, 0, 'call.data')
== adress(call.data[len(call.data)-1])
(MASK_SHL, 256, call.data.length-256, 0, 'call.data')
== call.data[0]
MASK_SHL is a super-unobvious construct, and it's hard to wrap your head around,
but simplifies very many things.
'''
'''
Reading storage:
(STORAGE, size, offset, num[, idx])
== MASK_SHL(size, offset, (Storage num[idx]))
E.g.
(STORAGE, 160, 0, 1)
== address at Storage#1
(STORAGE, 256, 0, 2, (MASK_SHL, 160, 0, 0, _from))
== Storage#2[addr(_from)] # so, Storage#2 is a mapping addr=>uint256
'''
'''
Writing storage:
(STORE, size, offset, num, idx, value)
E.g.
(STORE, 160, 0, 1, null, (MASK_SHL, 160, 0, 0, 'CALLER'))
writes function caller address to first 160 bits of Storage 1
(remaining bits of the storage stay untouched)
(STORE, 256, 0, 2, _receiver, (ADD, 1, (STORAGE, 256, 0, 2, _receiver)))
=> stor_2[_receiver] = stor_2[_receiver] + 1
'''
'''
(WHILE, condition, trace)
repeats trace execution until the condition is no longer true
'''
'''
(LOOP, trace, label)
where
trace == (line, line..., line, (END_LOOP, label) )
executes trace
'''
'''
(IF, condition, trace_if_true, trace_if_false)
An "if" statement
Important:
'if' statements is that they are always at the end of a trace
(i.e. there are no lines after the 'if' statement)
So, if a contract is following:
do_stuff
if condition:
do_A
else:
do_B
do_some_more_stuff
The trace will be like this:
[
do_stuff,
[IF, condition, [
do_A,
do_some_more_stuff
],[
do_B,
do_some_more_stuff
]
]
]
And never like this:
[
do_stuff,
[IF, condition, [do_A], [do_B]],
do_some_more_stuff
]
This makes the traces potentially extremely long, and difficult to read,
but has the benefit of extremely easy analysis
As an example, see:
http://eveem.org/code/0x06012c8cf97bead5deae237070f9587f8e7a266d.json
function
setSecondsPerBlock(uint256 _secs)
The human-readable, decompiled form (in 'print' attr in json, or on eveem.org) is short,
the trace is much longer
'''
'''
As for the other opcodes, they are not yet documented, but should be relatively understandable.
Put up a github issue if you have problems understanding:
https://github.com/kolinko/showmewhatyougot/issues
Also, the best way is to just print out the trace, and compare it to the decompiled output
on http://www.eveem.org/, or to the one in "print".
'''
def walk_trace(trace, f=print, knows_true=None):
'''
walks the trace, calling function f(line, knows_true) for every line
knows_true is a list of 'if' conditions that had to be met to reach a given
line
'''
res = []
knows_true = knows_true or []
for idx, line in enumerate(trace):
found = f(line, knows_true)
if found is not None:
res.append(found)
if opcode(line) == 'IF':
condition, if_true, if_false = line[1:]
res.extend(walk_trace(if_true, f, knows_true + [condition]))
res.extend(walk_trace(if_false, f, knows_true + [is_zero(condition)]))
assert idx == len(trace)-1, trace # IFs always end the trace tree
break
if opcode(line) == 'WHILE':
condition, while_trace = line[1:]
res.extend(walk_trace(while_trace, f, knows_true + [is_zero(condition)]))
continue
if opcode(line) == 'LOOP':
loop_trace, label = line[1:]
res.extend(walk_trace(loop_trace, f, knows_true))
return res
def walk_exp(exp, f=print):
'''
walks the expression - a more generic version of walk_trace.
useful for finding expressions of a given type (like 'all storages in the trace')
'''
found = f(exp)
res = [found] if found is not None else []
if type(exp) == tuple:
for e in exp:
res.extend(walk_exp(e, f))
return res