Safe SigningMethod

concept
safeprotocol-kitsignatureseip-191eip-712eip-1271

SigningMethod is the Safe Protocol Kit enum that selects between the three signature paths Safe supports for owners. It is passed as the second argument to protocolKit.signMessage(safeMessage, signingMethod) and to protocolKit.signTransaction(safeTx, signingMethod).

The three values

enum SigningMethod {
  ETH_SIGN = 'eth_sign',
  ETH_SIGN_TYPED_DATA_V4 = 'eth_signTypedData_v4',
  SAFE_SIGNATURE = 'safe_signature'
}

Each value routes the owner’s wallet through a different signing primitive and produces a differently-encoded SafeSignature in the EthSafeMessage accumulator.

ETH_SIGN — EIP-191 string signing

Used for plain string messages. The owner’s wallet computes:

ecdsa(keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))

This is the legacy path inherited from personal_sign. It exists because some applications still accept string-authenticated messages, and because every wallet supports it with no extra UI work. The signature is 65 bytes of (r, s, v) with v ∈ {31, 32} — the + 4 offset Safe uses to mark an eth_sign-derived signature in a multisig blob.

See Ethereum Signed Message Encoding for why the \x19 prefix exists and how it disambiguates from transactions and from EIP-712 digests.

ETH_SIGN_TYPED_DATA_V4 — EIP-712 typed data signing

Used for structured messages that conform to the EIP-712 type system. The owner’s wallet computes:

ecdsa("\x19\x01" ‖ domainSeparator ‖ hashStruct(message))

The wallet shows the structured message contents (field names and values) in its signing UI before the user consents. This is the modern default for off-chain signing on Ethereum — it provides human-readable signatures, domain separation, and deterministic encoding. Signature bytes are 65 bytes with v ∈ {27, 28} (unmodified, because EIP-712 is Safe’s canonical path).

SAFE_SIGNATURE — nested-Safe contract signature

Used when the owner of the Safe being signed for is itself another Safe. The signing Safe cannot produce an ECDSA signature (it has no private key), so the Protocol Kit recursively collects signatures from the inner Safe’s own owners and then wraps them into a single EIP-1271 contract signature for the outer Safe’s checkSignatures to resolve.

Call pattern:

signMessage(safeMessage, SigningMethod.SAFE_SIGNATURE, parentSafeAddress)

The third argument — parentSafeAddress — tells the nested Safe which domain its signature is being produced for, so the EIP-712 digest it signs is bound to the parent Safe’s context. Without this argument, a signature produced by a Safe for itself would not satisfy a different Safe’s isValidSignature call.

After the recursive collection, buildContractSignature produces the actual SafeSignature bytes that get added to the parent’s signature map. The resulting signature has v = 0, which is Safe’s discriminator for “this is a contract signature — call isValidSignature on the signer address to resolve it”.

The enum as a selector

SigningMethod is effectively a discriminated union tag. The same signMessage entry point routes through three very different code paths — wallet RPC calls, wallet RPC calls with different JSON, or recursive SDK calls plus contract-signature construction — depending on which enum value is passed. This is how the Protocol Kit collapses “every way a Safe owner can sign” into a single function signature.

The caller’s mental model: “I know what kind of owner is signing; I pick the matching enum value; the SDK does the right thing.” EOAs get ECDSA. Typed-data messages with EOAs get EIP-712. Nested Safes get the recursive contract-signature build. The accumulator (EthSafeMessage) stores all three kinds in the same map without the caller having to track which is which.

Connections