Skip to content

Commit d754891

Browse files
barragudaclaude
andcommitted
fix(protocol-kit): handle CONTRACT_SIGNATURE in toSafeTransactionType
`toSafeTransactionType` creates `EthSafeSignature` with `isContractSignature=false` for all confirmations, including CONTRACT_SIGNATURE types from the Safe Transaction Service. This causes `buildSignatureBytes` to concatenate the full stored signature blob (r+s+v+dynamic data) as-is instead of properly splitting it into static part (r, s=offset, v=0) and dynamic part (length + inner data). For multi-owner Safes, this breaks the combined signatures layout — the second signer's 65-byte constant part overlaps with the first signer's dynamic data, causing GS021 ("Invalid contract signature provided") on execution. The fix detects CONTRACT_SIGNATURE confirmations and: 1. Extracts the inner signature data from the stored format using the s offset (which points to the length-prefixed data) 2. Creates EthSafeSignature with isContractSignature=true 3. This lets buildSignatureBytes properly compute dynamic offsets Affects any Safe with EIP-1271 contract signature owners (passkey signers via SafeWebAuthnSignerProxy, nested Safes, etc.) when executing transactions that include contract signature confirmations fetched from the Safe Transaction Service. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8f8d296 commit d754891

1 file changed

Lines changed: 16 additions & 2 deletions

File tree

packages/protocol-kit/src/Safe.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,8 +1287,22 @@ class Safe {
12871287
})
12881288
serviceTransactionResponse.confirmations?.map(
12891289
(confirmation: SafeMultisigConfirmationResponse) => {
1290-
const signature = new EthSafeSignature(confirmation.owner, confirmation.signature)
1291-
safeTransaction.addSignature(signature)
1290+
if (confirmation.signatureType === 'CONTRACT_SIGNATURE') {
1291+
// Contract signatures are stored as: r(32) + s(32) + v(1) + dataLen(32) + data(N)
1292+
// Extract just the inner signature data that isValidSignature expects.
1293+
const sig = confirmation.signature.startsWith('0x')
1294+
? confirmation.signature.slice(2)
1295+
: confirmation.signature
1296+
// Read s (bytes 32-63) to find the byte offset to the length-prefixed data
1297+
const s = parseInt(sig.slice(64, 128), 16)
1298+
const dataLen = parseInt(sig.slice(s * 2, s * 2 + 64), 16)
1299+
const innerData = '0x' + sig.slice(s * 2 + 64, s * 2 + 64 + dataLen * 2)
1300+
const signature = new EthSafeSignature(confirmation.owner, innerData, true)
1301+
safeTransaction.addSignature(signature)
1302+
} else {
1303+
const signature = new EthSafeSignature(confirmation.owner, confirmation.signature)
1304+
safeTransaction.addSignature(signature)
1305+
}
12921306
}
12931307
)
12941308
return safeTransaction

0 commit comments

Comments
 (0)