Skip to content

Commit e99328e

Browse files
committed
feat(Wallet): add execution ordering via git-style branching
1 parent a87d368 commit e99328e

5 files changed

Lines changed: 31 additions & 7 deletions

File tree

contracts/Ruffsack.vy

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ EIP712_DOMAIN_TYPEHASH: constant(bytes32) = keccak256(
1515
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
1616
)
1717
MODIFY_TYPEHASH: constant(bytes32) = keccak256(
18-
"Modify(uint256 action,bytes data)"
18+
"Modify(bytes32 parent,uint256 action,bytes data)"
1919
)
2020

2121
struct Call:
@@ -29,7 +29,7 @@ CALL_TYPEHASH: constant(bytes32) = keccak256(
2929
"Call(address target,uint256 value,bytes data)"
3030
)
3131
EXECUTE_TYPEHASH: constant(bytes32) = keccak256(
32-
"Execute(Call[] calls)Call(address target,uint256 value,bytes data)"
32+
"Execute(bytes32 parent,Call[] calls)Call(address target,uint256 value,bytes data)"
3333
)
3434

3535
# @dev The current implementation address for `RuffsackProxy`
@@ -44,6 +44,9 @@ signers: public(DynArray[address, 11])
4444
threshold: public(uint256)
4545
# NOTE: invariant `0 < threshold <= len(signers)`
4646

47+
# @dev The last message hash (`Modify` or `Execute` struct) that was executed
48+
head: public(bytes32)
49+
4750
# @dev Set of pre-approved transaction hashes, indexed by signer
4851
approved: public(HashMap[bytes32, HashMap[address, bool]])
4952

@@ -247,10 +250,11 @@ def modify(
247250
# NOTE: Skip argument to use on-chain approvals
248251
):
249252
msghash: bytes32 = self._hash_typed_data_v4(
250-
# NOTE: Per EIP712, Dynamic ABI types are encoded as the hash of their contents
251-
keccak256(abi_encode(MODIFY_TYPEHASH, action, keccak256(data)))
253+
# NOTE: Per EIP712, Dynamic structures are encoded as the hash of their contents
254+
keccak256(abi_encode(MODIFY_TYPEHASH, self.head, action, keccak256(data)))
252255
)
253256
self._verify_signatures(msghash, signatures)
257+
self.head = msghash
254258

255259
admin_guard: IAdminGuard = self.admin_guard
256260
if admin_guard.address != empty(address):
@@ -332,9 +336,10 @@ def execute(
332336
# Step 3: Hash concatenated item hashes, together with typehash, then with domain to get msghash
333337
msghash: bytes32 = self._hash_typed_data_v4(
334338
# NOTE: Per EIP712, Arrays are encoded as the hash of their encoded members, concated together
335-
keccak256(abi_encode(EXECUTE_TYPEHASH, keccak256(encoded_call_array)))
339+
keccak256(abi_encode(EXECUTE_TYPEHASH, self.head, keccak256(encoded_call_array)))
336340
)
337341
self._verify_signatures(msghash, signatures)
342+
self.head = msghash
338343

339344
guard: IExecuteGuard = self.execute_guard
340345
for call: Call in calls:

ruffsack/messages/admin.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
class ModifyBase(EIP712Message):
1010
_name_ = "Ruffsack Wallet"
11+
parent: "bytes32"
1112
action: "uint256" # type: ignore
1213
data: "bytes"
1314

@@ -22,6 +23,7 @@ class ActionType(Flag):
2223

2324
def __call__(
2425
self,
26+
parent: bytes,
2527
*args,
2628
version: Version | None = None,
2729
address: AddressType | None = None,
@@ -46,6 +48,7 @@ class Modify(ModifyBase):
4648
arg_types = ("address",)
4749

4850
return Modify( # type: ignore[call-arg]
51+
parent=parent,
4952
action=self.value,
5053
data=abi_encode(arg_types, args),
5154
)

ruffsack/messages/execute.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import TYPE_CHECKING, Self
22

3+
from ape.types import HexBytes
34
from ape.utils import ManagerAccessMixin
45
from eip712 import EIP712Message, EIP712Type
56

@@ -17,17 +18,24 @@ class Call(EIP712Type):
1718
class ExecuteBase(EIP712Message):
1819
_name_ = "Ruffsack Wallet"
1920

21+
parent: "bytes32"
2022
calls: list[Call] = []
2123

2224

2325
class Execute(ManagerAccessMixin):
24-
def __init__(self, version: "Version", address: "AddressType", chain_id: int):
26+
def __init__(
27+
self,
28+
parent: HexBytes,
29+
version: "Version",
30+
address: "AddressType",
31+
chain_id: int,
32+
):
2533
class Execute(ExecuteBase):
2634
_verifyingContract_ = address
2735
_version_ = str(version)
2836
_chainId_ = chain_id
2937

30-
self.message = Execute()
38+
self.message = Execute(parent=parent)
3139

3240
def add_raw(self, target: "AddressType", value: int = 0, data: bytes = b"") -> Self:
3341
self.message.calls.append(Call(target=target, value=value, data=data))

tests/test_admin.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ def test_upgrade(
77
new_impl = create_release()
88

99
msg = ActionType.UPGRADE_IMPLEMENTATION(
10+
sack.head(),
1011
new_impl.address,
1112
version=VERSION,
1213
address=sack.address,
@@ -32,11 +33,14 @@ def test_upgrade(
3233
]
3334
assert sack.IMPLEMENTATION() == new_impl
3435

36+
assert sack.head() == msg._message_hash_
37+
3538

3639
def test_rotate_signers(
3740
accounts, chain, VERSION, THRESHOLD, sack, owners, approval_flow
3841
):
3942
msg = ActionType.ROTATE_SIGNERS(
43+
sack.head(),
4044
[accounts[len(owners)].address],
4145
[owners[0].address],
4246
sack.threshold(),
@@ -66,3 +70,5 @@ def test_rotate_signers(
6670
]
6771
assert sack.signers(0) == accounts[1]
6872
assert sack.signers(len(owners) - 1) == accounts[len(owners)]
73+
74+
assert sack.head() == msg._message_hash_

tests/test_operation.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def test_execute(
4040
accounts, chain, VERSION, THRESHOLD, owners, sack, approval_flow, calls
4141
):
4242
txn = Execute(
43+
parent=sack.head(),
4344
version=VERSION,
4445
address=sack.address,
4546
chain_id=chain.chain_id,
@@ -75,3 +76,4 @@ def test_execute(
7576
if total_calls > 0
7677
else []
7778
)
79+
assert sack.head() == txn.message._message_hash_

0 commit comments

Comments
 (0)