Signature Scoping
Some integrations require a Safe to produce a signature rather than execute a transaction — for example, signing a CowSwap order, a Permit2 approval, or a Sign-In with Ethereum (SIWE) message.
Because a Safe is a smart contract and cannot use a private key to sign, signatures are produced by writing to the Safe’s signedMessages storage and validated through EIP-1271 .
Marking a message as signed is itself a state-changing call from the Safe. The Roles Modifier scopes these signing calls just like any other transaction, but the data being checked is the message that’s about to be signed rather than the parameters of an external call.
How signing works
Signing is implemented by a small SignTypedMessageLib library that the Safe invokes via delegatecall.
The library exposes two entry points relevant to scoping:
signTypedMessage(domain, message, types)— for EIP-712 typed structured datapersonalSign(message)— for EIP-191personal_signtext messages
Each entry point computes the same SafeMessage hash that EIP-1271 verification expects, then sets signedMessages[hash] = 1 in the Safe’s storage.
After this, calling isValidSignature(hash, "0x") on the Safe returns the EIP-1271 magic value, so any verifier that follows the standard accepts the signature.
Because the call originates from the role member and goes through the Roles Modifier, the modifier’s conditions decide whether the signing call is allowed at all. That is the surface signature scoping operates on.
EIP-712 typed messages
EIP-712 messages are structured: a domain separator pins the signature to a specific app and chain, and a typed message tree describes the payload. Scoping a typed message means expressing constraints over those structured fields — for example “only allow CowSwap orders where the receiver is the avatar”.
To make this practical, SignTypedMessageLib does the EIP-712 encoding on-chain.
Roles permissions then evaluate ordinary conditions against the ABI-encoded domain and message blocks.
A field-level constraint on a typed message becomes a condition on the corresponding position in the encoded payload.
Selector aliases
A single Roles Modifier permission targets a function by its 4-byte selector. But a Safe may want to sign multiple unrelated EIP-712 layouts (e.g. CowSwap orders and Permit2 approvals) under different conditions.
To allow this, SignTypedMessageLib accepts signTypedMessage calls under any selector via a Solidity fallback.
The SDK derives a selector deterministically from the EIP-712 type definition (the keccak hash of its encoded type tree, truncated to 4 bytes), so each distinct typed-data layout gets its own permission entry.
Two permissions for two layouts coexist without conflict.
Type integrity
The types argument of signTypedMessage describes the layout the encoder uses to walk the domain and message bytes.
A malicious caller could try to pass a different types argument that re-interprets the same bytes as a different structure.
To prevent this, the scoped condition pins types to an exact value so only the layout the permission was authored for is accepted.
personal_sign messages
personal_sign messages are unstructured text wrapped with the EIP-191 prefix \x19Ethereum Signed Message:\n<length>.
There is no domain separator and no field tree to constrain — the only thing a permission can meaningfully match against is the message text itself.
The personalSign(bytes) entry point handles the EIP-191 wrapping internally, so the scoped condition operates on the raw message bytes.
The SDK’s prefix scoping uses a bitmask condition that requires the message to start with a given string.
This is enough for most real-world use cases:
- SIWE messages start with
"<domain> wants you to sign in with your Ethereum account", so pinning the domain prefix prevents phishing across hosts. - App-specific session messages typically start with a fixed marker that identifies the application.
The trailing portion of the message (nonces, timestamps, session IDs) is not constrained, since those are effectively single-use values that the verifying app is expected to validate.
SDK
For code examples covering both flows — defining permissions and producing the signing transaction calldata — see the SDK signing reference.