Skip to content

Use Safe as eth_call sender for EIP-1271 contract signature validation - #2577

Open
johanneskares wants to merge 2 commits into
safe-global:mainfrom
johanneskares:eip1271-call-from-safe
Open

Use Safe as eth_call sender for EIP-1271 contract signature validation#2577
johanneskares wants to merge 2 commits into
safe-global:mainfrom
johanneskares:eip1271-call-from-safe

Conversation

@johanneskares

Copy link
Copy Markdown

Motivation

Fixes the root cause behind safe-global/safe-transaction-service#2937.

On-chain, checkSignatures has the Safe itself call isValidSignature on its owner contracts, so msg.sender is always the Safe. The off-chain check in SafeSignatureContract._check_eip1271 performs a from-less eth_call (msg.sender = address(0)), which diverges from the on-chain semantics it is meant to predict.

For most owner contracts this makes no difference, but signatures from msg.sender-dependent owners always fail validation. The canonical example is the SafeWebAuthnSharedSigner from safe-modules (the standard gas-optimized ERC-4337 passkey setup): it stores each Safe's P-256 public key in the Safe's storage and reads it back via ISafe(msg.sender).getStorageAt(...) — in a from-less call there is no configuration to find, so pure-passkey Safes cannot propose transactions or register delegates with the Transaction Service at all.

Change

SafeSignatureContract.is_valid() (sync and async) already receives safe_address — other signature types like SafeSignatureApprovedHash use it — but the contract-signature path ignored it. This PR threads it into the EIP-1271 eth_call as the sender when provided:

  • safe_address given → isValidSignature is called with from = safe_address, mirroring on-chain checkSignatures.
  • safe_address not given → unchanged from-less call.

Owner contracts that ignore msg.sender (e.g. a Safe validating through its fallback handler) are unaffected.

Tests

Added sync + async tests deploying a minimal EIP-1271 signer that returns the magic value only when msg.sender matches a configured address (emulating the shared signer's behavior), asserting that validation fails without safe_address and succeeds with it. All existing tests in test_safe_signature.py pass (38 passed locally against a local node).

Notes for safe-transaction-service

The service's proposal endpoints already pass safe_address to is_valid, so they gain shared-signer support with a dependency bump and no code change. The delegates v2 endpoint currently passes the delegator as second argument (is_valid(ethereum_client, owner)); supporting shared-signer delegators there needs a small follow-up passing the request's safe — happy to submit that PR as well.

On-chain, checkSignatures has the Safe itself call isValidSignature on its
owner contracts, so msg.sender is always the Safe. The off-chain check
performed a from-less eth_call (msg.sender = address(0)), which diverges
from on-chain semantics and makes signatures from msg.sender-dependent
owners like the SafeWebAuthnSharedSigner always fail validation.

SafeSignatureContract.is_valid already receives safe_address (other
signature types use it); thread it into the EIP-1271 eth_call as the
sender when provided. Signers that ignore msg.sender are unaffected.
@johanneskares
johanneskares requested a review from a team as a code owner July 10, 2026 14:58
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@datadog-official

This comment has been minimized.

@johanneskares

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

1 similar comment
@johanneskares

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

@johanneskares

Copy link
Copy Markdown
Author

recheck

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant