• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

safe-global / safe-eth-py / 10793540350

10 Sep 2024 01:31PM UTC coverage: 93.551% (-0.3%) from 93.892%
10793540350

push

github

falvaradorodriguez
Fix cowswap test

8777 of 9382 relevant lines covered (93.55%)

3.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.14
/safe_eth/safe/safe_signature.py
1
from abc import ABC, abstractmethod
4✔
2
from enum import Enum
4✔
3
from logging import getLogger
4✔
4
from typing import List, Optional, Sequence, Union
4✔
5

6
from eth_abi import decode as decode_abi
4✔
7
from eth_abi import encode as encode_abi
4✔
8
from eth_abi.exceptions import DecodingError
4✔
9
from eth_account.messages import defunct_hash_message
4✔
10
from eth_typing import BlockIdentifier, ChecksumAddress, HexAddress, HexStr
4✔
11
from hexbytes import HexBytes
4✔
12
from web3.exceptions import Web3Exception
4✔
13

14
from safe_eth.eth import EthereumClient
4✔
15
from safe_eth.eth.contracts import (
4✔
16
    get_compatibility_fallback_handler_contract,
17
    get_safe_contract,
18
)
19
from safe_eth.eth.utils import fast_to_checksum_address
4✔
20
from safe_eth.safe.signatures import (
4✔
21
    get_signing_address,
22
    signature_split,
23
    signature_to_bytes,
24
)
25

26
logger = getLogger(__name__)
4✔
27

28

29
EthereumBytes = Union[bytes, str]
4✔
30

31

32
class SafeSignatureException(Exception):
4✔
33
    pass
4✔
34

35

36
class CannotCheckEIP1271ContractSignature(SafeSignatureException):
4✔
37
    pass
4✔
38

39

40
class SafeSignatureType(Enum):
4✔
41
    CONTRACT_SIGNATURE = 0
4✔
42
    APPROVED_HASH = 1
4✔
43
    EOA = 2
4✔
44
    ETH_SIGN = 3
4✔
45

46
    @staticmethod
4✔
47
    def from_v(v: int):
4✔
48
        if v == 0:
4✔
49
            return SafeSignatureType.CONTRACT_SIGNATURE
4✔
50
        elif v == 1:
4✔
51
            return SafeSignatureType.APPROVED_HASH
4✔
52
        elif v > 30:
4✔
53
            return SafeSignatureType.ETH_SIGN
4✔
54
        else:
55
            return SafeSignatureType.EOA
4✔
56

57

58
def uint_to_address(value: int) -> ChecksumAddress:
4✔
59
    """
60
    Convert a Solidity `uint` value to a checksummed `address`, removing
61
    invalid padding bytes if present
62

63
    :return: Checksummed address
64
    """
65
    encoded = encode_abi(["uint"], [value])
4✔
66
    # Remove padding bytes, as Solidity will ignore it but `eth_abi` will not
67
    encoded_without_padding_bytes = b"\x00" * 12 + encoded[-20:]
4✔
68
    return fast_to_checksum_address(
4✔
69
        decode_abi(["address"], encoded_without_padding_bytes)[0]
70
    )
71

72

73
class SafeSignature(ABC):
4✔
74
    def __init__(self, signature: EthereumBytes, safe_hash: EthereumBytes):
4✔
75
        """
76
        :param signature: Owner signature
77
        :param safe_hash: Signed hash for the Safe (message or transaction)
78
        """
79
        self.signature = HexBytes(signature)
4✔
80
        self.safe_hash = HexBytes(safe_hash)
4✔
81
        self.v, self.r, self.s = signature_split(self.signature)
4✔
82

83
    def __str__(self):
4✔
84
        return f"SafeSignature type={self.signature_type.name} owner={self.owner}"
4✔
85

86
    @classmethod
4✔
87
    def parse_signature(
4✔
88
        cls,
89
        signatures: EthereumBytes,
90
        safe_hash: EthereumBytes,
91
        safe_hash_preimage: Optional[EthereumBytes] = None,
92
        ignore_trailing: bool = True,
93
    ) -> List["SafeSignature"]:
94
        """
95
        :param signatures: One or more signatures appended. EIP1271 data at the end is supported.
96
        :param safe_hash: Signed hash for the Safe (message or transaction)
97
        :param safe_hash_preimage: ``safe_hash`` preimage for EIP1271 validation
98
        :param ignore_trailing: Ignore trailing data on the signature. Some libraries pad it and add some zeroes at
99
            the end
100
        :return: List of SafeSignatures decoded
101
        """
102
        if not signatures:
4✔
103
            return []
4✔
104
        elif isinstance(signatures, str):
4✔
105
            signatures = HexBytes(signatures)
×
106

107
        signature_size = 65  # For contract signatures there'll be some data at the end
4✔
108
        data_position = len(
4✔
109
            signatures
110
        )  # For contract signatures, to stop parsing at data position
111

112
        safe_signatures = []
4✔
113
        for i in range(0, len(signatures), signature_size):
4✔
114
            if (
4✔
115
                i >= data_position
116
            ):  # If contract signature data position is reached, stop
117
                break
4✔
118

119
            signature = signatures[i : i + signature_size]
4✔
120
            if ignore_trailing and len(signature) < 65:
4✔
121
                # Trailing stuff
122
                break
4✔
123
            v, r, s = signature_split(signature)
4✔
124
            signature_type = SafeSignatureType.from_v(v)
4✔
125
            safe_signature: "SafeSignature"
126
            if signature_type == SafeSignatureType.CONTRACT_SIGNATURE:
4✔
127
                if s < data_position:
4✔
128
                    data_position = s
4✔
129
                contract_signature_len = int.from_bytes(
4✔
130
                    signatures[s : s + 32], "big"
131
                )  # Len size is 32 bytes
132
                contract_signature = signatures[
4✔
133
                    s + 32 : s + 32 + contract_signature_len
134
                ]  # Skip array size (32 bytes)
135
                safe_signature = SafeSignatureContract(
4✔
136
                    signature,
137
                    safe_hash,
138
                    safe_hash_preimage or safe_hash,
139
                    contract_signature,
140
                )
141
            elif signature_type == SafeSignatureType.APPROVED_HASH:
4✔
142
                safe_signature = SafeSignatureApprovedHash(signature, safe_hash)
4✔
143
            elif signature_type == SafeSignatureType.EOA:
4✔
144
                safe_signature = SafeSignatureEOA(signature, safe_hash)
4✔
145
            elif signature_type == SafeSignatureType.ETH_SIGN:
4✔
146
                safe_signature = SafeSignatureEthSign(signature, safe_hash)
4✔
147

148
            safe_signatures.append(safe_signature)
4✔
149
        return safe_signatures
4✔
150

151
    @classmethod
4✔
152
    def export_signatures(cls, safe_signatures: Sequence["SafeSignature"]) -> HexBytes:
4✔
153
        """
154
        Takes a list of SafeSignature objects and exports them as a valid signature for the contract
155

156
        :param safe_signatures:
157
        :return: Valid signature for the Safe contract
158
        """
159

160
        signature = b""
4✔
161
        dynamic_part = b""
4✔
162
        dynamic_offset = len(safe_signatures) * 65
4✔
163
        # Signatures must be sorted by owner
164
        for safe_signature in sorted(safe_signatures, key=lambda s: s.owner.lower()):
4✔
165
            if isinstance(safe_signature, SafeSignatureContract):
4✔
166
                signature += signature_to_bytes(
4✔
167
                    safe_signature.v, safe_signature.r, dynamic_offset
168
                )
169
                # encode_abi adds {32 bytes offset}{32 bytes size}. We don't need offset
170
                contract_signature_padded = encode_abi(
4✔
171
                    ["bytes"], [safe_signature.contract_signature]
172
                )[32:]
173
                contract_signature = contract_signature_padded[
4✔
174
                    : 32 + len(safe_signature.contract_signature)
175
                ]
176
                dynamic_part += contract_signature
4✔
177
                dynamic_offset += len(contract_signature)
4✔
178
            else:
179
                signature += safe_signature.export_signature()
4✔
180
        return HexBytes(signature + dynamic_part)
4✔
181

182
    def export_signature(self) -> HexBytes:
4✔
183
        """
184
        Exports signature in a format that's valid individually. That's important for contract signatures, as it
185
        will fix the offset
186

187
        :return:
188
        """
189
        return self.signature
4✔
190

191
    @property
4✔
192
    @abstractmethod
4✔
193
    def owner(self):
4✔
194
        """
195
        :return: Decode owner from signature, without any further validation (signature can be not valid)
196
        """
197
        raise NotImplementedError
×
198

199
    @abstractmethod
4✔
200
    def is_valid(self, ethereum_client: EthereumClient, safe_address: str) -> bool:
4✔
201
        """
202
        :param ethereum_client: Required for Contract Signature and Approved Hash check
203
        :param safe_address: Required for Approved Hash check
204
        :return: `True` if signature is valid, `False` otherwise
205
        """
206
        raise NotImplementedError
×
207

208
    @property
4✔
209
    @abstractmethod
4✔
210
    def signature_type(self) -> SafeSignatureType:
4✔
211
        raise NotImplementedError
×
212

213

214
class SafeSignatureContract(SafeSignature):
4✔
215
    EIP1271_MAGIC_VALUE = HexBytes(0x20C13B0B)
4✔
216
    EIP1271_MAGIC_VALUE_UPDATED = HexBytes(0x1626BA7E)
4✔
217

218
    def __init__(
4✔
219
        self,
220
        signature: EthereumBytes,
221
        safe_hash: EthereumBytes,
222
        safe_hash_preimage: EthereumBytes,
223
        contract_signature: EthereumBytes,
224
    ):
225
        """
226
        :param signature:
227
        :param safe_hash: Signed hash for the Safe (message or transaction)
228
        :param safe_hash_preimage: ``safe_hash`` preimage for EIP1271 validation
229
        :param contract_signature:
230
        """
231
        super().__init__(signature, safe_hash)
4✔
232
        self.safe_hash_preimage = HexBytes(safe_hash_preimage)
4✔
233
        self.contract_signature = HexBytes(contract_signature)
4✔
234

235
    @classmethod
4✔
236
    def from_values(
4✔
237
        cls,
238
        safe_owner: ChecksumAddress,
239
        safe_hash: EthereumBytes,
240
        safe_hash_preimage: EthereumBytes,
241
        contract_signature: EthereumBytes,
242
    ) -> "SafeSignatureContract":
243
        signature = signature_to_bytes(
4✔
244
            0, int.from_bytes(HexBytes(safe_owner), byteorder="big"), 65
245
        )
246
        return cls(signature, safe_hash, safe_hash_preimage, contract_signature)
4✔
247

248
    @property
4✔
249
    def owner(self) -> ChecksumAddress:
4✔
250
        """
251
        :return: Address of contract signing. No further checks to get the owner are needed,
252
            but it could be a non-existing contract
253
        """
254

255
        return uint_to_address(self.r)
4✔
256

257
    @property
4✔
258
    def signature_type(self) -> SafeSignatureType:
4✔
259
        return SafeSignatureType.CONTRACT_SIGNATURE
4✔
260

261
    def export_signature(self) -> HexBytes:
4✔
262
        """
263
        Fix offset (s) and append `contract_signature` at the end of the signature
264

265
        :return:
266
        """
267
        # encode_abi adds {32 bytes offset}{32 bytes size}. We don't need offset
268
        contract_signature_padded = encode_abi(["bytes"], [self.contract_signature])[
4✔
269
            32:
270
        ]
271
        contract_signature = contract_signature_padded[
4✔
272
            : 32 + len(self.contract_signature)
273
        ]
274
        dynamic_offset = 65
4✔
275

276
        return HexBytes(
4✔
277
            signature_to_bytes(self.v, self.r, dynamic_offset) + contract_signature
278
        )
279

280
    def is_valid(self, ethereum_client: EthereumClient, *args) -> bool:
4✔
281
        compatibility_fallback_handler = get_compatibility_fallback_handler_contract(
4✔
282
            ethereum_client.w3, self.owner
283
        )
284
        is_valid_signature_fn = (
4✔
285
            compatibility_fallback_handler.get_function_by_signature(
286
                "isValidSignature(bytes,bytes)"
287
            )
288
        )
289
        try:
4✔
290
            return is_valid_signature_fn(
4✔
291
                self.safe_hash_preimage, self.contract_signature
292
            ).call() in (
293
                self.EIP1271_MAGIC_VALUE,
294
                self.EIP1271_MAGIC_VALUE_UPDATED,
295
            )
296
        except (Web3Exception, DecodingError, ValueError):
4✔
297
            # Error using `pending` block identifier or contract does not exist
298
            logger.warning(
4✔
299
                "Cannot check EIP1271 signature from contract %s", self.owner
300
            )
301
        return False
4✔
302

303

304
class SafeSignatureApprovedHash(SafeSignature):
4✔
305
    @property
4✔
306
    def owner(self):
4✔
307
        return uint_to_address(self.r)
4✔
308

309
    @property
4✔
310
    def signature_type(self):
4✔
311
        return SafeSignatureType.APPROVED_HASH
4✔
312

313
    @classmethod
4✔
314
    def build_for_owner(cls, owner: str, safe_hash: str) -> "SafeSignatureApprovedHash":
4✔
315
        r = owner.lower().replace("0x", "").rjust(64, "0")
4✔
316
        s = "0" * 64
4✔
317
        v = "01"
4✔
318
        return cls(HexBytes(r + s + v), safe_hash)
4✔
319

320
    def is_valid(self, ethereum_client: EthereumClient, safe_address: str) -> bool:
4✔
321
        safe_contract = get_safe_contract(
×
322
            ethereum_client.w3, ChecksumAddress(HexAddress(HexStr(safe_address)))
323
        )
324
        exception: Exception
325

326
        block_identifiers: List[BlockIdentifier] = ["pending", "latest"]
×
327
        for block_identifier in block_identifiers:
×
328
            try:
×
329
                return (
×
330
                    safe_contract.functions.approvedHashes(
331
                        self.owner, self.safe_hash
332
                    ).call(block_identifier=block_identifier)
333
                    == 1
334
                )
335
            except (Web3Exception, DecodingError, ValueError) as e:
×
336
                # Error using `pending` block identifier
337
                exception = e
×
338
        raise exception  # This should never happen
×
339

340

341
class SafeSignatureEthSign(SafeSignature):
4✔
342
    @property
4✔
343
    def owner(self):
4✔
344
        # defunct_hash_message prepends `\x19Ethereum Signed Message:\n32`
345
        message_hash = defunct_hash_message(primitive=self.safe_hash)
4✔
346
        return get_signing_address(message_hash, self.v - 4, self.r, self.s)
4✔
347

348
    @property
4✔
349
    def signature_type(self):
4✔
350
        return SafeSignatureType.ETH_SIGN
4✔
351

352
    def is_valid(self, *args) -> bool:
4✔
353
        return True
4✔
354

355

356
class SafeSignatureEOA(SafeSignature):
4✔
357
    @property
4✔
358
    def owner(self):
4✔
359
        return get_signing_address(self.safe_hash, self.v, self.r, self.s)
4✔
360

361
    @property
4✔
362
    def signature_type(self):
4✔
363
        return SafeSignatureType.EOA
4✔
364

365
    def is_valid(self, *args) -> bool:
4✔
366
        return True
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc