-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathCaravan.vy
More file actions
365 lines (296 loc) · 11.8 KB
/
Caravan.vy
File metadata and controls
365 lines (296 loc) · 11.8 KB
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
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# pragma version 0.4.3
"""
@title Caravan
@license Apache-2.0
@author ApeWorX LTD.
"""
from . import ICaravan
implements: ICaravan
from .guards import IAdminGuard
from .guards import IExecuteGuard
NAME: constant(String[15]) = "Caravan Wallet"
NAMEHASH: constant(bytes32) = keccak256(NAME)
# NOTE: Update this before each release (controls EIP712 Domain)
VERSION: public(immutable(String[12]))
VERSIONHASH: immutable(bytes32)
EIP712_DOMAIN_TYPEHASH: constant(bytes32) = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
)
MODIFY_TYPEHASH: constant(bytes32) = keccak256(
"Modify(bytes32 parent,uint256 action,bytes data)"
)
CALL_TYPEHASH: constant(bytes32) = keccak256(
"Call(address target,uint256 value,bool success_required,bytes data)"
)
EXECUTE_TYPEHASH: constant(bytes32) = keccak256(
"Execute(bytes32 parent,Call[] calls)Call(address target,uint256 value,bool success_required,bytes data)"
)
# @dev The current implementation address for `CaravanProxy`
IMPLEMENTATION: public(address)
# NOTE: Must be first slot, this will be used by upgradeable proxy for delegation
# @dev The last message hash (`Modify` or `Execute` struct) that was executed
head: public(bytes32)
# @dev Mapping of pre-approved transaction hashes to approval timestamps, indexed by signer
approved: public(HashMap[bytes32, HashMap[address, uint256]])
# Signer properties
# @dev All current signers (unordered)
_signers: DynArray[address, 11]
# @dev Number of signers required to execute an action
threshold: public(uint256)
# NOTE: invariant `0 < threshold <= len(signers)`
# @dev Before/after checker for Update actions
admin_guard: public(IAdminGuard)
# @dev Before/after checker for Execute actions
execute_guard: public(IExecuteGuard)
# @dev Modules enabled for this wallet
module_enabled: public(HashMap[address, bool])
# NOTE: Future variables (used for new core features) must be added below
@deploy
def __init__(version: String[12]):
VERSION = version
VERSIONHASH = keccak256(version)
# NOTE: IERC5267
@view
@external
def eip712Domain() -> (
bytes1,
String[50],
String[20],
uint256,
address,
bytes32,
DynArray[uint256, 32],
):
return (
# NOTE: `0x0f` equals `01111` (`salt` is not used)
0x0f,
NAME,
VERSION,
chain.id,
self,
empty(bytes32), # Salt is ignored
empty(DynArray[uint256, 32]), # No extensions
)
@view
def _DOMAIN_SEPARATOR() -> bytes32:
return keccak256(
abi_encode(EIP712_DOMAIN_TYPEHASH, NAMEHASH, VERSIONHASH, chain.id, self)
)
@view
@external
def DOMAIN_SEPARATOR() -> bytes32:
return self._DOMAIN_SEPARATOR()
@view
def _hash_typed_data_v4(struct_hash: bytes32) -> bytes32:
return keccak256(concat(x"1901", self._DOMAIN_SEPARATOR(), struct_hash))
@external
def initialize(signers: DynArray[address, 11], threshold: uint256):
assert self.IMPLEMENTATION != empty(address) # dev: only Proxy can initialize
assert self.threshold == 0 # dev: can only initialize once
assert threshold > 0 and threshold <= len(signers)
self._signers = signers
self.threshold = threshold
# NOTE: Initialize head to non-zero, network-specific value as if this action was performed
self.head = self._hash_typed_data_v4(
keccak256(
abi_encode(
MODIFY_TYPEHASH,
empty(bytes32),
ICaravan.ActionType.ROTATE_SIGNERS,
# NOTE: Per EIP712, Dynamic structures are encoded as the hash of their contents
keccak256(
abi_encode(
signers, empty(DynArray[address, 11]), threshold
)
),
)
)
)
@view
@external
def signers() -> DynArray[address, 11]:
return self._signers
@external
def set_approval(msghash: bytes32, approved: bool = True):
assert msg.sender in self._signers, "Not a signer"
if approved:
self.approved[msghash][msg.sender] = block.timestamp
else:
self.approved[msghash][msg.sender] = 0
def _verify_signatures(msghash: bytes32, signatures: DynArray[Bytes[65], 11]):
approvals_needed: uint256 = self.threshold
signers: DynArray[address, 11] = self._signers
already_approved: DynArray[address, 11] = []
for signer: address in signers:
if self.approved[msghash][signer] <= block.timestamp:
already_approved.append(signer) # NOTE: Track for use in next loop
approvals_needed -= 1 # dev: underflow
# NOTE: Get some gas back by deleting storage
self.approved[msghash][signer] = 0
if approvals_needed == 0:
return # Skip signature verification because we have enough pre-approvals
assert len(signatures) >= approvals_needed, "Not enough approvals"
# NOTE: We already checked that we have enough signatures,
# this loops checks uniqueness/membership of recovered signers
for sig: Bytes[65] in signatures:
# NOTE: Signatures should be 65 bytes in RSV order
r: bytes32 = convert(slice(sig, 0, 32), bytes32)
s: bytes32 = convert(slice(sig, 32, 32), bytes32)
v: uint8 = convert(slice(sig, 64, 1), uint8)
signer: address = ecrecover(msghash, v, r, s)
assert signer in signers, "Invalid Signer"
assert signer not in already_approved, "Signer cannot approve twice"
already_approved.append(signer)
def _rotate_signers(
signers_to_add: DynArray[address, 11],
signers_to_rm: DynArray[address, 11],
threshold: uint256,
):
current_signers: DynArray[address, 11] = self._signers
new_signers: DynArray[address, 11] = []
for signer: address in current_signers:
if signer not in signers_to_rm:
new_signers.append(signer)
# else: skips adding `signer` to `new_signers`
# NOTE: Ignores if `signer` in `signers_to_rm` not in `current_signers`
for signer: address in signers_to_add:
assert signer not in new_signers, "Signer cannot be added twice"
new_signers.append(signer)
if threshold > 0:
assert threshold <= len(new_signers), "Invalid threshold"
self.threshold = threshold
self._signers = new_signers
log ICaravan.SignersRotated(
executor=msg.sender,
num_signers=len(new_signers),
threshold=self.threshold, # NOTE: In case there was no change
signers_added=signers_to_add,
signers_removed=signers_to_rm,
)
@external
def modify(
action: ICaravan.ActionType,
data: Bytes[65535],
signatures: DynArray[Bytes[65], 11] = [],
# NOTE: Skip argument to use on-chain approvals
):
msghash: bytes32 = self._hash_typed_data_v4(
# NOTE: Per EIP712, Dynamic structures are encoded as the hash of their contents
keccak256(abi_encode(MODIFY_TYPEHASH, self.head, action, keccak256(data)))
)
self._verify_signatures(msghash, signatures)
self.head = msghash
admin_guard: IAdminGuard = self.admin_guard
if admin_guard.address != empty(address):
extcall admin_guard.preUpdateCheck(action, data)
if action == ICaravan.ActionType.UPGRADE_IMPLEMENTATION:
new: address = abi_decode(data, address)
log ICaravan.ImplementationUpgraded(
executor=msg.sender,
old=self.IMPLEMENTATION,
new=new,
)
self.IMPLEMENTATION = new
elif action == ICaravan.ActionType.ROTATE_SIGNERS:
signers_to_add: DynArray[address, 11] = []
signers_to_rm: DynArray[address, 11] = []
threshold: uint256 = 0
signers_to_add, signers_to_rm, threshold = abi_decode(
data,
(DynArray[address, 11], DynArray[address, 11], uint256),
)
self._rotate_signers(signers_to_add, signers_to_rm, threshold)
elif action == ICaravan.ActionType.CONFIGURE_MODULE:
module: address = empty(address)
enabled: bool = False
module, enabled = abi_decode(data, (address, bool))
log ICaravan.ModuleUpdated(
executor=msg.sender,
module=module,
enabled=enabled,
)
self.module_enabled[module] = enabled
elif action == ICaravan.ActionType.SET_ADMIN_GUARD:
# NOTE: Don't use `admin_guard` as it would override above
guard: IAdminGuard = abi_decode(data, IAdminGuard)
log ICaravan.AdminGuardUpdated(
executor=msg.sender,
old=admin_guard.address,
new=guard.address,
)
self.admin_guard = guard
elif action == ICaravan.ActionType.SET_EXECUTE_GUARD:
guard: IExecuteGuard = abi_decode(data, IExecuteGuard)
log ICaravan.ExecuteGuardUpdated(
executor=msg.sender,
old=self.execute_guard.address,
new=guard.address,
)
self.execute_guard = guard
else:
raise "Unsupported"
if admin_guard.address != empty(address):
# NOTE: We use the old admin guard to execute the check
extcall admin_guard.postUpdateCheck()
@external
def execute(
calls: DynArray[ICaravan.Call, 8],
signatures: DynArray[Bytes[65], 11] = [],
# NOTE: Skip argument to use on-chain approvals, or for module use
):
if not self.module_enabled[msg.sender]:
# Hash message and validate signatures
# Step 1: Encode struct to list of 32 byte hash of items
encoded_call_members: DynArray[bytes32, 8] = []
for call: ICaravan.Call in calls:
encoded_call_members.append(
# NOTE: Per EIP712, structs are encoded as the hash of their contents (incl. Typehash)
keccak256(
abi_encode(
CALL_TYPEHASH,
call.target,
call.value,
call.success_required,
# NOTE: Per EIP712, Dynamic ABI types are encoded as the hash of their contents
keccak256(call.data),
)
)
)
# Step 2: Encode list of 32 byte items into single bytestring
# NOTE: bytestring length including length because it's encoded as an array
encoded_call_array: Bytes[32 * (8 + 1)] = abi_encode(encoded_call_members, ensure_tuple=False)
# NOTE: Skip encoded length of encoded bytestring by slicing it off (start at byte 32)
encoded_call_array = slice(encoded_call_array, 32, len(encoded_call_array) - 32)
assert len(encoded_call_array) == 32 * len(calls)
# Step 3: Hash concatenated item hashes, together with typehash, then with domain to get msghash
msghash: bytes32 = self._hash_typed_data_v4(
# NOTE: Per EIP712, Arrays are encoded as the hash of their encoded members, concated together
keccak256(abi_encode(EXECUTE_TYPEHASH, self.head, keccak256(encoded_call_array)))
)
self._verify_signatures(msghash, signatures)
self.head = msghash
guard: IExecuteGuard = self.execute_guard
for call: ICaravan.Call in calls:
if guard.address != empty(address):
extcall guard.preExecuteCheck(call)
# NOTE: No delegatecalls allowed (cannot modify configuration via `update` this way)
success: bool = True
if call.success_required:
# NOTE: Allow failure to bubble up normally
raw_call(call.target, call.data, value=call.value)
else:
success = raw_call(
call.target,
call.data,
value=call.value,
revert_on_failure=False,
)
if guard.address != empty(address):
extcall guard.postExecuteCheck()
log ICaravan.Executed(
executor=msg.sender,
success=success,
target=call.target,
value=call.value,
data=call.data,
)