Skip to content

Commit ab9c372

Browse files
committed
Fix 1271 contract signatures
- Closes #2535 and #2526
1 parent ac49e26 commit ab9c372

File tree

5 files changed

+81
-44
lines changed

5 files changed

+81
-44
lines changed

safe_transaction_service/safe_messages/serializers.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@
99
from rest_framework import serializers
1010
from rest_framework.exceptions import ValidationError
1111
from safe_eth.eth import get_auto_ethereum_client
12-
from safe_eth.eth.eip712 import eip712_encode_hash
12+
from safe_eth.eth.eip712 import eip712_encode
1313
from safe_eth.safe.safe_signature import SafeSignature, SafeSignatureType
1414
from safe_eth.util.util import to_0x_hex_str
1515

1616
from safe_transaction_service.utils.serializers import get_safe_owners
1717

1818
from .models import SIGNATURE_LENGTH, SafeMessage, SafeMessageConfirmation
19-
from .utils import get_hash_for_message, get_safe_message_hash_for_message
19+
from .utils import (
20+
get_message_encoded,
21+
get_safe_message_hash_and_preimage_for_message,
22+
)
2023

2124

2225
# Request serializers
@@ -93,7 +96,7 @@ def validate_message(self, value: Union[str, dict[str, Any]]):
9396

9497
if isinstance(value, dict):
9598
try:
96-
eip712_encode_hash(value)
99+
eip712_encode(value)
97100
return value
98101
except ValueError as exc:
99102
raise ValidationError(
@@ -105,12 +108,15 @@ def validate_message(self, value: Union[str, dict[str, Any]]):
105108
def validate(self, attrs):
106109
attrs = super().validate(attrs)
107110
safe_address = self.context["safe_address"]
111+
attrs["safe"] = safe_address
108112
message = attrs["message"]
109113
signature = attrs["signature"]
110-
attrs["safe"] = safe_address
111-
message_hash = get_hash_for_message(message)
112-
safe_message_hash = get_safe_message_hash_for_message(
113-
safe_address, message_hash
114+
# Encode EIP-191 or EIP-712 original message as bytes
115+
message_encoded = get_message_encoded(message)
116+
safe_message_hash, safe_message_preimage = (
117+
get_safe_message_hash_and_preimage_for_message(
118+
safe_address, message_encoded
119+
)
114120
)
115121
attrs["message_hash"] = safe_message_hash
116122

@@ -119,8 +125,11 @@ def validate(self, attrs):
119125
f"Message with hash {to_0x_hex_str(safe_message_hash)} for safe {safe_address} already exists in DB"
120126
)
121127

128+
# Preimage is encoded for the Safe. But if an EIP-1271 signature is used, owner's Safe will be called
129+
# the preimage will be encoded again for the owner Safe. That's what needs to be signed by the user
130+
# So original data -> EIP-191 or EIP-712 encoded -> Safe encoded data -> Owner encoded data
122131
safe_signatures = SafeSignature.parse_signature(
123-
signature, safe_message_hash, safe_hash_preimage=message_hash
132+
signature, safe_message_hash, safe_hash_preimage=safe_message_preimage
124133
)
125134
owner, signature_type = self.get_valid_owner_from_signatures(
126135
safe_signatures, safe_address, None
@@ -158,11 +167,16 @@ def validate(self, attrs):
158167
attrs["safe_message"] = safe_message
159168
signature: HexStr = attrs["signature"]
160169
safe_address = safe_message.safe
161-
message_hash = get_hash_for_message(safe_message.message)
162-
safe_message_hash = safe_message.message_hash
170+
message_encoded = get_message_encoded(safe_message.message)
171+
safe_message_hash, safe_message_preimage = (
172+
get_safe_message_hash_and_preimage_for_message(
173+
safe_address, message_encoded
174+
)
175+
)
176+
assert to_0x_hex_str(safe_message_hash) == safe_message.message_hash
163177

164178
safe_signatures = SafeSignature.parse_signature(
165-
signature, safe_message_hash, safe_hash_preimage=message_hash
179+
signature, safe_message_hash, safe_hash_preimage=safe_message_preimage
166180
)
167181
owner, signature_type = self.get_valid_owner_from_signatures(
168182
safe_signatures, safe_address, safe_message

safe_transaction_service/safe_messages/tests/factories.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from safe_eth.util.util import to_0x_hex_str
66

77
from ..models import SafeMessage, SafeMessageConfirmation
8-
from ..utils import get_hash_for_message, get_safe_message_hash_for_message
8+
from ..utils import get_message_encoded, get_safe_message_hash_and_preimage_for_message
99

1010

1111
class SafeMessageFactory(DjangoModelFactory):
@@ -21,9 +21,9 @@ class Meta:
2121
@factory.lazy_attribute
2222
def message_hash(self) -> str:
2323
return to_0x_hex_str(
24-
get_safe_message_hash_for_message(
25-
self.safe, get_hash_for_message(self.message)
26-
)
24+
get_safe_message_hash_and_preimage_for_message(
25+
self.safe, get_message_encoded(self.message)
26+
)[0]
2727
)
2828

2929

safe_transaction_service/safe_messages/tests/test_models.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from safe_eth.safe.tests.safe_test_case import SafeTestCaseMixin
99
from safe_eth.util.util import to_0x_hex_str
1010

11-
from ..utils import get_hash_for_message, get_safe_message_hash_for_message
11+
from ..utils import get_message_encoded, get_safe_message_hash_and_preimage_for_message
1212
from .factories import SafeMessageConfirmationFactory, SafeMessageFactory
1313
from .mocks import get_eip712_payload_mock
1414

@@ -22,15 +22,15 @@ def test_str(self):
2222
for input, expected in [
2323
(
2424
"TestMessage",
25-
"Safe Message 0xb04a24aa07a51d1d8c3913e9493b3b1f88ed6a8a75430a9a8eda3ed3ce1897bc - TestMessage",
25+
"Safe Message 0x95a89792bad17115e101b4d8fbdcd517dd13d085e815287d7fe791078182bf9e - TestMessage",
2626
),
2727
(
2828
"TestMessageVeryLong",
29-
"Safe Message 0xe3db816540ce371e2703b8ec59bdd6fec32e0c6078f2e204a205fd6d81564f28 - TestMessageVery...",
29+
"Safe Message 0xb7783fde9060e75b61298c14cbc2a987f0e0800d1bb08b4a2b24ce3544b9b144 - TestMessageVery...",
3030
),
3131
(
3232
get_eip712_payload_mock(),
33-
"Safe Message 0xbabb22f5c02a24db447b8f0136d6e26bb58cd6d068ebe8ab25c2221cfdf53e18 - {'types': {'EIP...",
33+
"Safe Message 0x632db20317ce8e467d17e941d209fc1173ff1cad83b0c072c008385a566ddd80 - {'types': {'EIP...",
3434
),
3535
]:
3636
with self.subTest(input=input):
@@ -61,9 +61,9 @@ def test_factory(self):
6161
self.assertEqual(
6262
message_hash,
6363
to_0x_hex_str(
64-
get_safe_message_hash_for_message(
65-
safe_message.safe, get_hash_for_message(message)
66-
)
64+
get_safe_message_hash_and_preimage_for_message(
65+
safe_message.safe, get_message_encoded(message)
66+
)[0]
6767
),
6868
)
6969
recovered_owner = Account._recover_hash(

safe_transaction_service/safe_messages/tests/test_views.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@
77

88
import eth_abi
99
from eth_account import Account
10-
from eth_account.messages import defunct_hash_message
1110
from hexbytes import HexBytes
1211
from rest_framework import status
1312
from rest_framework.exceptions import ErrorDetail
1413
from rest_framework.test import APITestCase
15-
from safe_eth.eth.eip712 import eip712_encode_hash
14+
from safe_eth.eth.eip712 import eip712_encode
1615
from safe_eth.safe.safe_signature import SafeSignatureEOA
1716
from safe_eth.safe.signatures import signature_to_bytes
1817
from safe_eth.safe.tests.safe_test_case import SafeTestCaseMixin
@@ -28,6 +27,7 @@
2827
)
2928
from safe_transaction_service.utils.utils import datetime_to_str
3029

30+
from ..utils import encode_eip191_message, encode_eip712_message
3131
from .mocks import get_eip712_payload_mock
3232

3333
logger = logging.getLogger(__name__)
@@ -144,12 +144,13 @@ def test_safe_messages_create_view(self, get_owners_mock: MagicMock):
144144
safe_address = safe.address
145145
messages = ["Text to sign message", get_eip712_payload_mock()]
146146
description = "Testing EIP191 message signing"
147-
message_hashes = [
148-
defunct_hash_message(text=messages[0]),
149-
eip712_encode_hash(messages[1]),
147+
messages_encoded = [
148+
encode_eip191_message(messages[0]),
149+
encode_eip712_message(messages[1]),
150150
]
151151
safe_message_hashes = [
152-
safe.get_message_hash(message_hash) for message_hash in message_hashes
152+
safe.get_message_hash(message_encoded)
153+
for message_encoded in messages_encoded
153154
]
154155
signatures = [
155156
to_0x_hex_str(account.unsafe_sign_hash(safe_message_hash)["signature"])
@@ -158,15 +159,14 @@ def test_safe_messages_create_view(self, get_owners_mock: MagicMock):
158159

159160
sub_tests = ["create_eip191", "create_eip712"]
160161

161-
for sub_test, message, message_hash, safe_message_hash, signature in zip(
162-
sub_tests, messages, message_hashes, safe_message_hashes, signatures
162+
for sub_test, message, safe_message_hash, signature in zip(
163+
sub_tests, messages, safe_message_hashes, signatures
163164
):
164165
SafeMessage.objects.all().delete()
165166
get_owners_mock.return_value = []
166167
with self.subTest(
167168
sub_test,
168169
message=message,
169-
message_hash=message_hash,
170170
safe_message_hash=safe_message_hash,
171171
signature=signature,
172172
):
@@ -289,8 +289,13 @@ def _test_safe_messages_create_using_1271_signature_view(self, safe_deployment_f
289289
safe_address = safe.address
290290
message = get_eip712_payload_mock()
291291
description = "Testing EIP712 message signing"
292-
message_hash = eip712_encode_hash(message)
293-
safe_owner_message_hash = safe_owner.get_message_hash(message_hash)
292+
message_encoded = b"".join(eip712_encode(message))
293+
safe_message_hash, safe_message_preimage = safe.get_message_hash_and_preimage(
294+
message_encoded
295+
)
296+
safe_owner_message_hash, _ = safe_owner.get_message_hash_and_preimage(
297+
safe_message_preimage
298+
)
294299
safe_owner_signature = account.unsafe_sign_hash(safe_owner_message_hash)[
295300
"signature"
296301
]
@@ -314,8 +319,12 @@ def _test_safe_messages_create_using_1271_signature_view(self, safe_deployment_f
314319
data=data,
315320
)
316321
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
317-
self.assertEqual(SafeMessage.objects.count(), 1)
318-
self.assertEqual(SafeMessageConfirmation.objects.count(), 1)
322+
self.assertEqual(
323+
SafeMessage.objects.get().message_hash, to_0x_hex_str(safe_message_hash)
324+
)
325+
self.assertEqual(
326+
SafeMessageConfirmation.objects.get().owner, safe_owner.address
327+
)
319328

320329
def test_safe_messages_create_using_1271_signature_v1_3_0_view(self):
321330
return self._test_safe_messages_create_using_1271_signature_view(
Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,36 @@
11
from typing import Any
22

3-
from eth_account.messages import defunct_hash_message
3+
from eth_account.messages import encode_defunct
44
from eth_typing import ChecksumAddress, Hash32
55
from safe_eth.eth import get_auto_ethereum_client
6-
from safe_eth.eth.eip712 import eip712_encode_hash
6+
from safe_eth.eth.eip712 import eip712_encode
77
from safe_eth.safe import Safe
88

99

10-
def get_hash_for_message(message: str | dict[str, Any]) -> Hash32:
10+
def encode_eip191_message(message: str) -> bytes:
11+
signable_message = encode_defunct(text=message)
1112
return (
12-
defunct_hash_message(text=message)
13+
b"\x19"
14+
+ signable_message.version
15+
+ signable_message.header
16+
+ signable_message.body
17+
)
18+
19+
20+
def encode_eip712_message(message: dict[str, Any]) -> bytes:
21+
return b"".join(eip712_encode(message))
22+
23+
24+
def get_message_encoded(message: str | dict[str, Any]) -> bytes:
25+
return (
26+
encode_eip191_message(message)
1327
if isinstance(message, str)
14-
else eip712_encode_hash(message)
28+
else encode_eip712_message(message)
1529
)
1630

1731

18-
def get_safe_message_hash_for_message(
19-
safe_address: ChecksumAddress, message_hash: Hash32
20-
) -> Hash32:
32+
def get_safe_message_hash_and_preimage_for_message(
33+
safe_address: ChecksumAddress, message: bytes
34+
) -> tuple[Hash32, bytes]:
2135
safe = Safe(safe_address, get_auto_ethereum_client())
22-
return safe.get_message_hash(message_hash)
36+
return safe.get_message_hash_and_preimage(message)

0 commit comments

Comments
 (0)