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
- Create a 2/N Safe with at least one EIP-1271 contract signer (e.g. Safe's own
SafeWebAuthnSignerProxy for passkey owners)
- Submit a CONTRACT_SIGNATURE confirmation to the TX Service
- Fetch the transaction via
getTransaction() (which calls toSafeTransactionType)
- Call
executeTransaction() → reverts with GS021
Expected Behavior
toSafeTransactionType should detect CONTRACT_SIGNATURE confirmations and:
- Extract the inner signature data from the stored format (using the
s offset)
- 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
Bug Description
toSafeTransactionTypeinpackages/protocol-kit/src/Safe.tscreatesEthSafeSignaturewithisContractSignature=false(the default) for all confirmations from the Safe Transaction Service, including those withsignatureType: 'CONTRACT_SIGNATURE'.This causes
buildSignatureBytesto concatenate the full stored signature blob as a non-contract signature (theelsebranch), instead of properly splitting it into a static part (with computed dynamic offset) and dynamic part (length-prefixed inner data).Current code (line ~1290):
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)(viaexport_signature()). WhenbuildSignatureBytesdumps this full blob as-is (becauseisContractSignature=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
SafeWebAuthnSignerProxyfor passkey owners)getTransaction()(which callstoSafeTransactionType)executeTransaction()→ reverts with GS021Expected Behavior
toSafeTransactionTypeshould detectCONTRACT_SIGNATUREconfirmations and:soffset)EthSafeSignature(owner, innerData, true)sobuildSignatureBytesproperly handles the static/dynamic splitEnvironment
@safe-global/protocol-kit: latest (main branch)SafeWebAuthnSignerProxy(0x12367d0e...) deployed via Safe's officialSafeWebAuthnSignerFactory