-
Notifications
You must be signed in to change notification settings - Fork 4
/
bmc.py
273 lines (208 loc) · 9.32 KB
/
bmc.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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
from z3 import *
# set_param(proof=True)
set_param(unsat_core=True)
# helper funcs and boilerplate. sets up a test framework
predicates = {}
my_proofs = {}
def require(s, assertion):
# black magic introspection shit
import traceback,ast
frame = traceback.extract_stack()[-2]
code = frame.line
yea = ast.parse(code)
yea = list(ast.iter_child_nodes(next(ast.iter_child_nodes(next(ast.iter_child_nodes(yea))))))[2]
yea = ast.unparse(yea)
p = FreshBool()
predicates[p] = (yea, frame.lineno, frame.name)
s.assert_and_track(assertion, p)
def print_unsat_core(s):
for p in s.unsat_core():
code, lineno, name = predicates[p]
print(f'* {str(p):5} {"line " + str(lineno):9} {name:16} {code}')
def my_proof(s, name=None):
def decorating_function(user_function):
if name is None:
assert(user_function.__name__.startswith('proof_'))
_name = user_function.__name__[6:]
else:
_name = name # shadowing bullshit
def decorated_function(*args, **kwargs):
s.push()
user_function(*args, **kwargs)
assert(s.check() == unsat)
print('Unsat core:')
print_unsat_core(s)
s.pop()
my_proofs[_name] = decorated_function
return decorated_function
return decorating_function
def run_proof(name):
func = my_proofs[name]
print(name)
func()
print('-> ok')
def run_proofs():
for name in my_proofs:
run_proof(name)
# actual WETH stuff
AddressSort = BitVecSort(160)
Address = lambda x: BitVecVal(x, AddressSort)
UintSort = BitVecSort(256)
Uint = lambda x: BitVecVal(x, UintSort)
WETH_Address = BitVec('WETH_Address', AddressSort)
MAX_ETH = Uint(120000000e18)
# WETH state = tuple (balanceOf: Array<..>, allowance: Array<..>)
# global state = tuple (balances: ArraySort(AddressSort, UintSort), WETH state)
# deposit() = tuple (msg.sender: AddressSort, msg.value: UintSort)
def deposit(s, state, msg_sender, msg_value):
eth_balances, weth_state = state
balanceOf, allowance = weth_state
# implicit from how EVM works
require(s, UGE(eth_balances[msg_sender], msg_value))
eth_balances = Store(eth_balances, msg_sender, eth_balances[msg_sender] - msg_value)
eth_balances = Store(eth_balances, WETH_Address, eth_balances[WETH_Address] + msg_value)
# balanceOf[msg.sender] += msg.value;
balanceOf = Store(balanceOf, msg_sender, balanceOf[msg_sender] + msg_value)
# Deposit(msg.sender, msg.value);
return (eth_balances, (balanceOf, allowance))
# withdraw() = tuple (msg.sender: AddressSort, wad: UintSort)
def withdraw(s, state, msg_sender, wad):
eth_balances, weth_state = state
balanceOf, allowance = weth_state
# require(balanceOf[msg.sender] >= wad);
require(s, UGE(balanceOf[msg_sender], wad))
# balanceOf[msg.sender] -= wad;
balanceOf = Store(balanceOf, msg_sender, balanceOf[msg_sender] - wad)
# msg.sender.transfer(wad);
require(s, UGE(eth_balances[WETH_Address], wad))
eth_balances = Store(eth_balances, msg_sender, eth_balances[msg_sender] + wad)
eth_balances = Store(eth_balances, WETH_Address, eth_balances[WETH_Address] - wad)
# Withdrawal(msg.sender, wad);
return (eth_balances, (balanceOf, allowance))
def approve(s, state, msg_sender, guy, wad):
eth_balances, weth_state = state
balanceOf, allowance = weth_state
# allowance[msg.sender][guy] = wad;
allowance = Store(allowance, msg_sender, Store(allowance[msg_sender], guy, wad))
# Approval(msg.sender, guy, wad);
return (eth_balances, (balanceOf, allowance))
def transfer(s, state, msg_sender, dst, wad):
return transferFrom(s, state, msg_sender, msg_sender, dst, wad)
def transferFrom(s, state, msg_sender, src, dst, wad):
eth_balances, weth_state = state
balanceOf, allowance = weth_state
# require(balanceOf[src] >= wad);
require(s, UGE(balanceOf[src], wad))
p = And(src != msg_sender, allowance[src][msg_sender] != Uint(0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff))
require(s, Implies(p, UGE(allowance[src][msg_sender], wad)))
allowance = If(p, Store(allowance, src, Store(allowance[src], msg_sender, allowance[src][msg_sender] - wad)), allowance)
balanceOf = Store(balanceOf, src, balanceOf[src]-wad)
balanceOf = Store(balanceOf, dst, balanceOf[dst]+wad)
return (eth_balances, (balanceOf, allowance))
def initial_state():
s = Solver()
myUser = Const('myUser', AddressSort)
initialDeposit = Const('initialDeposit', UintSort)
balanceOf = Array('balanceOf', AddressSort, UintSort)
allowance = Array('allowance', AddressSort, ArraySort(AddressSort, UintSort))
eth_balances = Array('eth_balances', AddressSort, UintSort)
# This is a manually defined constraint.
# We proved that in horn.py, but this lemma needs to be manually imported.
a = Const('a', AddressSort)
require(s, ForAll([a], ULE(balanceOf[a], eth_balances[WETH_Address])))
require(s, ForAll([a], ULE(eth_balances[a], MAX_ETH)))
# assumptions
balanceOf = Store(balanceOf, myUser, Uint(0))
require(s, myUser != WETH_Address)
require(s, ForAll([a], allowance[myUser][a] == 0))
starting_balance = eth_balances[myUser]
weth_state = (balanceOf, allowance)
state = (eth_balances, weth_state)
state = deposit(s, state, myUser, initialDeposit)
return s, state, myUser, initialDeposit, starting_balance
def is_ok(s, state, myUser, initialDeposit, starting_balance):
s2 = Solver() # create a new solver and load it up with the assertions from a simulated withdrawal by our depositor
new_state = withdraw(s2, state, myUser, initialDeposit)
eth_balances, weth_state = new_state
p = And(s2.assertions()) # copy all of those assertions over into our predicate
p = And(p, eth_balances[myUser] == starting_balance) # final balance check. technically optional, second clause always true if p is true
return p
# any external call to deposit
def symbolic_deposit(s, state, myUser):
user = FreshConst(AddressSort, 'user')
value = FreshConst(UintSort, 'value')
require(s, user != WETH_Address)
require(s, user != myUser)
state = deposit(s, state, user, value)
return state
# any external call to withdraw
def symbolic_withdraw(s, state, myUser):
user = FreshConst(AddressSort, 'user')
wad = FreshConst(UintSort, 'wad')
require(s, user != WETH_Address)
require(s, user != myUser)
state = withdraw(s, state, user, wad)
return state
def symbolic_approve(s, state, myUser):
user = FreshConst(AddressSort, 'user')
guy = FreshConst(AddressSort, 'guy')
wad = FreshConst(UintSort, 'wad')
require(s, user != WETH_Address)
require(s, user != myUser)
state = approve(s, state, user, guy, wad)
return state
def symbolic_transfer(s, state, myUser):
user = FreshConst(AddressSort, 'user')
dst = FreshConst(AddressSort, 'dst')
wad = FreshConst(UintSort, 'wad')
require(s, user != WETH_Address)
require(s, user != myUser)
state = transfer(s, state, user, dst, wad)
return state
def symbolic_transferFrom(s, state, myUser):
user = FreshConst(AddressSort, 'user')
src = FreshConst(AddressSort, 'src')
dst = FreshConst(AddressSort, 'dst')
wad = FreshConst(UintSort, 'wad')
require(s, user != WETH_Address)
require(s, user != myUser)
state = transferFrom(s, state, user, src, dst, wad)
return state
# ok let's actually prove shit
s, state, myUser, initialDeposit, starting_balance = initial_state()
def sanity_check(cur_state, starting_balance):
# sanity check. Let's make sure the initial state is valid
s.push()
s.add(Not(is_ok(s, cur_state, myUser, initialDeposit, starting_balance)))
assert(s.check() == unsat)
s.pop()
sanity_check(state, starting_balance)
# each of these proofs is an inductive step.
# Given a (valid) initial state, prove that the final state after an
# arbitrary state transition is NOT invalid.
# Therefore, it would be impossible to reach an invalid state.
# each of these sub-proofs is a subgoal that checks a possible class
# of state transitions.
# together, they show that after any arbitrary transaction (within certain limitations)
# cannot break our invariants.
@my_proof(s)
def proof_deposit():
new_state = symbolic_deposit(s, state, myUser)
require(s, Not(is_ok(s, new_state, myUser, initialDeposit, starting_balance)))
@my_proof(s)
def proof_withdraw():
new_state = symbolic_withdraw(s, state, myUser)
require(s, Not(is_ok(s, new_state, myUser, initialDeposit, starting_balance)))
@my_proof(s)
def proof_approve():
new_state = symbolic_approve(s, state, myUser)
require(s, Not(is_ok(s, new_state, myUser, initialDeposit, starting_balance)))
@my_proof(s)
def proof_transfer():
new_state = symbolic_transfer(s, state, myUser)
require(s, Not(is_ok(s, new_state, myUser, initialDeposit, starting_balance)))
@my_proof(s)
def proof_transferFrom():
new_state = symbolic_transferFrom(s, state, myUser)
require(s, Not(is_ok(s, new_state, myUser, initialDeposit, starting_balance)))
run_proofs()