Skip to content

Commit 3cee7b7

Browse files
Fix 1271 signatures for delegates (#2555)
* Fix 1271 signatures for delegates * Fix tests * Add test with nested safes --------- Co-authored-by: Uxio Fuentefria <[email protected]> Co-authored-by: Felipe Alvarado <[email protected]>
1 parent 735a168 commit 3cee7b7

File tree

4 files changed

+107
-24
lines changed

4 files changed

+107
-24
lines changed

safe_transaction_service/history/helpers.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
from eth_typing import ChecksumAddress, Hash32, HexStr
66
from eth_utils import keccak
7-
from safe_eth.eth.eip712 import eip712_encode_hash
7+
from safe_eth.eth.eip712 import eip712_encode, eip712_encode_hash
8+
from safe_eth.eth.utils import fast_keccak
89

910
from safe_transaction_service.history.models import TransferDict
1011
from safe_transaction_service.tokens.models import Token
@@ -81,19 +82,19 @@ def calculate_hash(
8182

8283
class DelegateSignatureHelperV2(TemporarySignatureHelper):
8384
@classmethod
84-
def calculate_hash(
85+
def calculate_hash_and_preimage(
8586
cls,
8687
delegate_address: ChecksumAddress,
8788
chain_id: Optional[int],
8889
previous_totp: bool = False,
89-
) -> Hash32:
90+
) -> tuple[Hash32, bytes]:
9091
"""
9192
Builds a EIP712 object and calculates its hash
9293
9394
:param delegate_address:
9495
:param chain_id:
95-
:param previous_totp:
96-
:return: Hash for the EIP712 generated object from the provided parameters
96+
:param previous_totp: if true calculate previous totp interval
97+
:return: Hash for the EIP712 generated object from the provided parameters with the preimage
9798
"""
9899
totp = cls.calculate_totp(previous=previous_totp)
99100

@@ -125,7 +126,8 @@ def calculate_hash(
125126
)
126127
payload["domain"]["chainId"] = chain_id
127128

128-
return eip712_encode_hash(payload)
129+
preimage = b"".join(eip712_encode(payload))
130+
return fast_keccak(preimage), preimage
129131

130132

131133
def is_valid_unique_transfer_id(unique_transfer_id: str) -> bool:

safe_transaction_service/history/serializers.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -452,14 +452,20 @@ def validate_delegator_signature(
452452
) -> bool:
453453
ethereum_client = get_auto_ethereum_client()
454454
chain_id = ethereum_client.get_chain_id()
455+
any_valid_signature_found = False
456+
455457
# Accept a message with the current topt and the previous totp (to prevent replay attacks)
456458
for previous_totp, chain_id in list(
457459
itertools.product((True, False), (chain_id, None))
458460
):
459-
message_hash = DelegateSignatureHelperV2.calculate_hash(
460-
delegate, chain_id, previous_totp=previous_totp
461+
message_hash, preimage = (
462+
DelegateSignatureHelperV2.calculate_hash_and_preimage(
463+
delegate, chain_id, previous_totp=previous_totp
464+
)
465+
)
466+
safe_signatures = SafeSignature.parse_signature(
467+
signature, message_hash, safe_hash_preimage=preimage
461468
)
462-
safe_signatures = SafeSignature.parse_signature(signature, message_hash)
463469
if not safe_signatures:
464470
raise ValidationError("Signature is not valid")
465471

@@ -469,13 +475,15 @@ def validate_delegator_signature(
469475
)
470476
safe_signature = safe_signatures[0]
471477
owner = safe_signature.owner
472-
if not safe_signature.is_valid(ethereum_client, owner):
473-
raise ValidationError(
474-
f"Signature of type={safe_signature.signature_type.name} "
475-
f"for signer={signer} is not valid"
476-
)
477-
if owner == signer:
478-
return True
478+
479+
if safe_signature.is_valid(ethereum_client, owner):
480+
any_valid_signature_found = True
481+
if owner == signer:
482+
return True
483+
484+
if not any_valid_signature_found:
485+
raise ValidationError(f"No valid signature found for signer={signer}")
486+
479487
return False
480488

481489

safe_transaction_service/history/tests/test_helpers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def test_calculate_hash(self):
2424
"0x40ef28feb993bf127265260d48ab1a427d5b852aa8b38f511fcd368bfc9cbfdf"
2525
)
2626

27-
result_hash = DelegateSignatureHelperV2.calculate_hash(
27+
result_hash, _ = DelegateSignatureHelperV2.calculate_hash_and_preimage(
2828
delegate_address, chain_id, False
2929
)
3030

@@ -36,7 +36,7 @@ def test_calculate_hash(self):
3636
"0x37fe1c77d467e92109ef91637e30a4053bab963de2ea74f9d6c4ba1918ff32e6"
3737
)
3838

39-
result_hash = DelegateSignatureHelperV2.calculate_hash(
39+
result_hash, _ = DelegateSignatureHelperV2.calculate_hash_and_preimage(
4040
delegate_address, chain_id, True
4141
)
4242

safe_transaction_service/history/tests/test_views_v2.py

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ def test_delegates_post(self):
232232
# Create delegate
233233
self.assertEqual(SafeContractDelegate.objects.count(), 0)
234234
chain_id = self.ethereum_client.get_chain_id()
235-
hash_to_sign = DelegateSignatureHelperV2.calculate_hash(
235+
hash_to_sign, _ = DelegateSignatureHelperV2.calculate_hash_and_preimage(
236236
delegate.address, chain_id, False
237237
)
238238
data["signature"] = to_0x_hex_str(
@@ -271,7 +271,7 @@ def test_delegates_post(self):
271271
self.assertIsNone(safe_contract_delegate.expiry_date)
272272

273273
# Create delegate without a Safe
274-
hash_to_sign = DelegateSignatureHelperV2.calculate_hash(
274+
hash_to_sign, _ = DelegateSignatureHelperV2.calculate_hash_and_preimage(
275275
delegate.address, chain_id, False
276276
)
277277
data = {
@@ -293,7 +293,7 @@ def test_delegates_post(self):
293293
data["signature"] = to_0x_hex_str(signature)
294294
response = self.client.post(url, format="json", data=data)
295295
self.assertIn(
296-
f"Signature of type=CONTRACT_SIGNATURE for signer={delegator.address} is not valid",
296+
f"No valid signature found for signer={delegator.address}",
297297
response.data["non_field_errors"][0],
298298
)
299299
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
@@ -302,7 +302,7 @@ def test_delegate_creation_without_chain_id(self):
302302
chain_id = None
303303
delegate = Account.create()
304304
delegator = Account.create()
305-
hash_to_sign = DelegateSignatureHelperV2.calculate_hash(
305+
hash_to_sign, _ = DelegateSignatureHelperV2.calculate_hash_and_preimage(
306306
delegate.address, chain_id, False
307307
)
308308
data = {
@@ -326,7 +326,7 @@ def test_delegate_creation_without_chain_id_with_safe(self):
326326
chain_id = None
327327
delegate = Account.create()
328328
delegator = Account.create()
329-
hash_to_sign = DelegateSignatureHelperV2.calculate_hash(
329+
hash_to_sign, _ = DelegateSignatureHelperV2.calculate_hash_and_preimage(
330330
delegate.address, chain_id, False
331331
)
332332
data = {
@@ -354,6 +354,79 @@ def test_delegate_creation_without_chain_id_with_safe(self):
354354
self.assertEqual(safe_contract_delegate.delegator, delegator.address)
355355
self.assertEqual(safe_contract_delegate.safe_contract_id, safe.address)
356356

357+
def _test_add_delegate_using_1271_signature(self, safe_deployment_fn):
358+
account_a = Account.create()
359+
account_b = Account.create()
360+
safe_owner = safe_deployment_fn(
361+
owners=[account_a.address, account_b.address], threshold=2
362+
)
363+
SafeContractFactory(address=safe_owner.address)
364+
365+
nested_safe = safe_deployment_fn(owners=[safe_owner.address])
366+
SafeContractFactory(address=nested_safe.address)
367+
368+
chain_id = self.ethereum_client.get_chain_id()
369+
delegate = Account.create()
370+
371+
_, preimage_to_sign = DelegateSignatureHelperV2.calculate_hash_and_preimage(
372+
delegate.address, chain_id, previous_totp=False
373+
)
374+
375+
safe_owner_message_hash, _ = safe_owner.get_message_hash_and_preimage(
376+
preimage_to_sign
377+
)
378+
379+
safe_owner_account_a_signature = account_a.unsafe_sign_hash(
380+
safe_owner_message_hash
381+
)["signature"]
382+
safe_owner_account_b_signature = account_b.unsafe_sign_hash(
383+
safe_owner_message_hash
384+
)["signature"]
385+
386+
# Build EIP1271 signature v=0 r=safe v=dynamic_part dynamic_part=size+owner_signature
387+
signatures_with_addresses = [
388+
(account_a.address.lower(), safe_owner_account_a_signature),
389+
(account_b.address.lower(), safe_owner_account_b_signature),
390+
]
391+
signatures_with_addresses.sort(key=lambda x: x[0])
392+
ordered_signatures = b"".join(sig for _, sig in signatures_with_addresses)
393+
394+
signature_1271 = (
395+
signature_to_bytes(
396+
0, int.from_bytes(HexBytes(safe_owner.address), byteorder="big"), 65
397+
)
398+
+ eth_abi.encode(["bytes"], [ordered_signatures])[32:]
399+
)
400+
401+
data = {
402+
"label": "Nested safe delegator",
403+
"safe": nested_safe.address,
404+
"delegate": delegate.address,
405+
"delegator": safe_owner.address,
406+
"signature": to_0x_hex_str(HexBytes(signature_1271)),
407+
}
408+
response = self.client.post(
409+
reverse("v2:history:delegates"),
410+
format="json",
411+
data=data,
412+
)
413+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
414+
self.assertEqual(SafeContractDelegate.objects.count(), 1)
415+
safe_contract_delegate = SafeContractDelegate.objects.get()
416+
self.assertEqual(safe_contract_delegate.delegate, delegate.address)
417+
self.assertEqual(safe_contract_delegate.delegator, safe_owner.address)
418+
self.assertEqual(safe_contract_delegate.safe_contract_id, nested_safe.address)
419+
420+
def test_add_delegate_using_1271_signature_v1_3_0(self):
421+
return self._test_add_delegate_using_1271_signature(
422+
self.deploy_test_safe_v1_3_0
423+
)
424+
425+
def test_add_delegate_using_1271_signature_v1_4_1(self):
426+
return self._test_add_delegate_using_1271_signature(
427+
self.deploy_test_safe_v1_4_1
428+
)
429+
357430
def test_delegates_get(self):
358431
url = reverse("v2:history:delegates")
359432
response = self.client.get(url, format="json")
@@ -432,7 +505,7 @@ def test_delegate_delete(self):
432505
delegate = Account.create()
433506
delegator = Account.create()
434507
chain_id = self.ethereum_client.get_chain_id()
435-
hash_to_sign = DelegateSignatureHelperV2.calculate_hash(
508+
hash_to_sign, _ = DelegateSignatureHelperV2.calculate_hash_and_preimage(
436509
delegate.address, chain_id, False
437510
)
438511
# Test delete using delegate signature and then delegator signature

0 commit comments

Comments
 (0)