Skip to content

protocol-kit: toSafeTransactionType doesn't handle CONTRACT_SIGNATURE confirmations correctly #1300

@indexedfit

Description

@indexedfit

Bug Description

toSafeTransactionType in packages/protocol-kit/src/Safe.ts creates EthSafeSignature with isContractSignature=false (the default) for all confirmations from the Safe Transaction Service, including those with signatureType: 'CONTRACT_SIGNATURE'.

This causes buildSignatureBytes to concatenate the full stored signature blob as a non-contract signature (the else branch), instead of properly splitting it into a static part (with computed dynamic offset) and dynamic part (length-prefixed inner data).

Current code (line ~1290):

serviceTransactionResponse.confirmations?.map(
  (confirmation: SafeMultisigConfirmationResponse) => {
    const signature = new EthSafeSignature(confirmation.owner, confirmation.signature)
    safeTransaction.addSignature(signature)
  }
)

Impact

For any Safe with EIP-1271 contract signature owners (e.g. passkey signers via SafeWebAuthnSignerProxy, nested Safes), executing a transaction with confirmations fetched from the TX Service fails with GS021 ("Invalid contract signature provided").

Root cause: The TX Service stores contract signatures as r(32) + s(32) + v(1) + dataLen(32) + innerData(N) (via export_signature()). When buildSignatureBytes dumps this full blob as-is (because isContractSignature=false), the second signer's 65-byte constant part slot overlaps with the first signer's dynamic data, corrupting the combined signatures layout.

Steps to Reproduce

  1. Create a 2/N Safe with at least one EIP-1271 contract signer (e.g. Safe's own SafeWebAuthnSignerProxy for passkey owners)
  2. Submit a CONTRACT_SIGNATURE confirmation to the TX Service
  3. Fetch the transaction via getTransaction() (which calls toSafeTransactionType)
  4. Call executeTransaction() → reverts with GS021

Expected Behavior

toSafeTransactionType should detect CONTRACT_SIGNATURE confirmations and:

  1. Extract the inner signature data from the stored format (using the s offset)
  2. Create EthSafeSignature(owner, innerData, true) so buildSignatureBytes properly handles the static/dynamic split

Environment

  • @safe-global/protocol-kit: latest (main branch)
  • Chain: Ethereum mainnet
  • Contract signer: SafeWebAuthnSignerProxy (0x12367d0e...) deployed via Safe's official SafeWebAuthnSignerFactory

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions