Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 121 additions & 44 deletions safe_eth/safe/safe_signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from eth_account.messages import defunct_hash_message
from eth_typing import BlockIdentifier, ChecksumAddress, HexAddress, HexStr
from hexbytes import HexBytes
from packaging.version import InvalidVersion, Version
from typing_extensions import Self
from web3 import AsyncWeb3, Web3
from web3.contract import Contract
Expand Down Expand Up @@ -337,10 +338,14 @@ def is_valid(
self,
ethereum_client: Optional[EthereumClient] = None,
safe_address: Optional[str] = None,
safe_version: Optional[str] = None,
) -> bool:
"""
:param ethereum_client: Required for Contract Signature and Approved Hash check
:param safe_address: Required for Approved Hash check
:param safe_version: Version of the Safe that will verify this signature. Required for
a correct Contract Signature check, as the version decides which EIP-1271 entrypoint
is called on-chain. Ignored by every other signature type.
:return: `True` if signature is valid, `False` otherwise
"""
raise NotImplementedError
Expand All @@ -352,10 +357,48 @@ async def is_valid(
self,
web3: Optional[AsyncWeb3] = None,
safe_address: Optional[str] = None,
safe_version: Optional[str] = None,
) -> bool:
"""
:param web3: Required for Contract Signature and Approved Hash check
:param safe_address: Required for Approved Hash check
:param safe_version: Version of the Safe that will verify this signature. Required for
a correct Contract Signature check, as the version decides which EIP-1271 entrypoint
is called on-chain. Ignored by every other signature type.
:return: `True` if signature is valid, `False` otherwise
"""
raise NotImplementedError


# From this version a Safe verifies contract signatures through
# `isValidSignature(bytes32,bytes)`; below it, through the legacy
# `isValidSignature(bytes,bytes)`.
EIP1271_BYTES32_MIN_VERSION = Version("1.5.0")


def uses_bytes32_eip1271(safe_version: str) -> bool:
"""Whether a Safe of `safe_version` verifies contract signatures with the hash.

The single home for the rule: `checkContractSignature` on a Safe >= 1.5.0 calls
`isValidSignature(bytes32,bytes)` with `dataHash`, while `checkNSignatures` below that
calls the legacy `isValidSignature(bytes,bytes)` with the whole preimage. Callers that
report a rejection need the same answer to name the entrypoint, so they should ask here
rather than compare against `EIP1271_BYTES32_MIN_VERSION` themselves.

:param safe_version: Version of the Safe that verifies the signature.
:return: `True` from 1.5.0 on, `False` below.
:raises CannotCheckEIP1271ContractSignature: If `safe_version` cannot be compared. Raised
instead of `packaging`'s `InvalidVersion` so callers handle one Safe-domain error and
do not each reimplement the translation. `VERSION()` is an arbitrary on-chain string.
"""
try:
return Version(safe_version) >= EIP1271_BYTES32_MIN_VERSION
except InvalidVersion as exc:
raise CannotCheckEIP1271ContractSignature(
f"Cannot tell which EIP-1271 entrypoint Safe version {safe_version!r} uses"
) from exc


class SafeSignatureContractMixin(SafeSignatureBase):
EIP1271_MAGIC_VALUE = HexBytes(0x20C13B0B)
EIP1271_MAGIC_VALUE_UPDATED = HexBytes(0x1626BA7E)
Expand Down Expand Up @@ -614,40 +657,53 @@ def is_valid(
self,
ethereum_client: Optional[EthereumClient] = None,
safe_address: Optional[str] = None,
safe_version: Optional[str] = None,
) -> bool:
"""
Validate the signature using the appropriate EIP-1271 path.
First tries the updated method (bytes32,bytes).
Falls back to the legacy method (bytes,bytes).
Validate the signature through the EIP-1271 entrypoint the verifying Safe uses.

:param ethereum_client: EthereumClient instance
:param safe_address: Optional Safe EthereumAddress instance.
:param safe_version: Version of the Safe that will verify this signature. Only the
entrypoint that version calls on-chain is checked. Defaults to `None`, which checks
both and so can return `True` for a signature that reverts `GS024` on that Safe;
pass a version whenever it is known.
:raises CannotCheckEIP1271ContractSignature: If `safe_version` cannot be compared.
"""
if ethereum_client is None:
raise ValueError(
"ethereum_client is required to validate contract signature"
)

for fallback_handler_getter, function_signature, data in (
(
get_compatibility_fallback_handler_contract,
"isValidSignature(bytes32,bytes)",
bytes(self.safe_hash),
),
(
get_compatibility_fallback_handler_V1_4_1_contract,
"isValidSignature(bytes,bytes)",
bytes(self.safe_hash_preimage),
),
# Only one entrypoint is ever called on-chain and the verifying Safe's version
# decides which: `checkContractSignature` on a Safe >= 1.5.0 hands `dataHash` to the
# signer, while `checkNSignatures` below that hands over the whole preimage. `None`
# means the caller does not know, so both are tried.
if safe_version is None:
try_bytes32 = try_legacy = True
else:
try_bytes32 = uses_bytes32_eip1271(safe_version)
try_legacy = not try_bytes32

if try_bytes32 and self._check_eip1271(
ethereum_client,
get_compatibility_fallback_handler_contract,
"isValidSignature(bytes32,bytes)",
bytes(self.safe_hash),
bytes(self.contract_signature),
):
Comment thread
Uxio0 marked this conversation as resolved.
if self._check_eip1271(
ethereum_client,
fallback_handler_getter,
function_signature,
data,
bytes(self.contract_signature),
):
return True
return True

# Dropped from the 1.5.0 `CompatibilityFallbackHandler`, so a 1.5.0 signer does not
# declare this overload at all and can never answer here
if try_legacy and self._check_eip1271(
ethereum_client,
get_compatibility_fallback_handler_V1_4_1_contract,
"isValidSignature(bytes,bytes)",
bytes(self.safe_hash_preimage),
bytes(self.contract_signature),
):
return True

return False

Expand All @@ -657,6 +713,7 @@ def is_valid(
self,
ethereum_client: Optional[EthereumClient] = None,
safe_address: Optional[str] = None,
safe_version: Optional[str] = None,
) -> bool:
if ethereum_client is None:
raise ValueError("ethereum_client is required to validate approved hash")
Expand Down Expand Up @@ -689,6 +746,7 @@ def is_valid(
self,
ethereum_client: Optional[EthereumClient] = None,
safe_address: Optional[str] = None,
safe_version: Optional[str] = None,
) -> bool:
return True

Expand All @@ -698,6 +756,7 @@ def is_valid(
self,
ethereum_client: Optional[EthereumClient] = None,
safe_address: Optional[str] = None,
safe_version: Optional[str] = None,
) -> bool:
return True

Expand All @@ -707,6 +766,7 @@ def is_valid(
self,
ethereum_client: Optional[EthereumClient] = None,
safe_address: Optional[str] = None,
safe_version: Optional[str] = None,
) -> bool:
return self._is_valid()

Expand Down Expand Up @@ -757,38 +817,51 @@ async def is_valid(
self,
web3: Optional[AsyncWeb3] = None,
safe_address: Optional[str] = None,
safe_version: Optional[str] = None,
) -> bool:
"""
Validate the signature using the appropriate EIP-1271 path.
First tries the updated method (bytes32,bytes).
Falls back to the legacy method (bytes,bytes).
Validate the signature through the EIP-1271 entrypoint the verifying Safe uses.

:param web3: Optional EthereumClient instance.
:param safe_address: Optional Safe EthereumAddress instance.
:param safe_version: Version of the Safe that will verify this signature. Only the
entrypoint that version calls on-chain is checked. Defaults to `None`, which checks
both and so can return `True` for a signature that reverts `GS024` on that Safe;
pass a version whenever it is known.
:raises CannotCheckEIP1271ContractSignature: If `safe_version` cannot be compared.
"""
if web3 is None:
raise ValueError("web3 is required to validate contract signature")

for fallback_handler_getter, function_signature, data in (
(
get_compatibility_fallback_handler_contract,
"isValidSignature(bytes32,bytes)",
bytes(self.safe_hash),
),
(
get_compatibility_fallback_handler_V1_4_1_contract,
"isValidSignature(bytes,bytes)",
bytes(self.safe_hash_preimage),
),
# Only one entrypoint is ever called on-chain and the verifying Safe's version
# decides which: `checkContractSignature` on a Safe >= 1.5.0 hands `dataHash` to the
# signer, while `checkNSignatures` below that hands over the whole preimage. `None`
# means the caller does not know, so both are tried.
if safe_version is None:
try_bytes32 = try_legacy = True
else:
try_bytes32 = uses_bytes32_eip1271(safe_version)
try_legacy = not try_bytes32

if try_bytes32 and await self._check_eip1271(
web3,
get_compatibility_fallback_handler_contract,
"isValidSignature(bytes32,bytes)",
bytes(self.safe_hash),
bytes(self.contract_signature),
):
if await self._check_eip1271(
web3,
fallback_handler_getter,
function_signature,
data,
bytes(self.contract_signature),
):
return True
return True

# Dropped from the 1.5.0 `CompatibilityFallbackHandler`, so a 1.5.0 signer does not
# declare this overload at all and can never answer here
if try_legacy and await self._check_eip1271(
web3,
get_compatibility_fallback_handler_V1_4_1_contract,
"isValidSignature(bytes,bytes)",
bytes(self.safe_hash_preimage),
bytes(self.contract_signature),
):
return True

return False

Expand All @@ -800,6 +873,7 @@ async def is_valid(
self,
web3: Optional[AsyncWeb3] = None,
safe_address: Optional[str] = None,
safe_version: Optional[str] = None,
) -> bool:
if web3 is None:
raise ValueError("web3 is required to validate approved hash")
Expand Down Expand Up @@ -830,6 +904,7 @@ async def is_valid(
self,
web3: Optional[AsyncWeb3] = None,
safe_address: Optional[str] = None,
safe_version: Optional[str] = None,
) -> bool:
return True

Expand All @@ -839,6 +914,7 @@ async def is_valid(
self,
web3: Optional[AsyncWeb3] = None,
safe_address: Optional[str] = None,
safe_version: Optional[str] = None,
) -> bool:
return True

Expand All @@ -848,6 +924,7 @@ async def is_valid(
self,
web3: Optional[AsyncWeb3] = None,
safe_address: Optional[str] = None,
safe_version: Optional[str] = None,
) -> bool:
return self._is_valid()

Expand Down
Loading
Loading