Safe Nested Contract Signatures
A Safe can be owned by other Safes. When that happens, the inner Safes must produce EIP-1271 contract signatures that the outer Safe’s checkSignatures can resolve. The Safe Protocol Kit automates this recursive flow through SigningMethod.SAFE_SIGNATURE and buildContractSignature.
The configuration
Consider a 3/4 Safe at SAFE_3_4_ADDRESS whose four owners are:
OWNER_1_ADDRESS— an EOAOWNER_2_ADDRESS— an EOASAFE_1_1_ADDRESS— a 1/1 Safe whose sole owner isOWNER_3_ADDRESS(an EOA)SAFE_2_3_ADDRESS— a 2/3 Safe whose owners are three EOAs, threshold 2
To sign a message with SAFE_3_4_ADDRESS, three of its four owners must sign. That means two EOAs can sign directly (ECDSA), one nested Safe must produce a 1-of-1 contract signature, and the other nested Safe must produce a 2-of-3 contract signature. The outer Safe does not care how the inner Safes reached their consent — it only needs a single SafeSignature per owner, encoded in the form its on-chain checkSignatures function can resolve.
Step 1: Each nested Safe collects its own signatures
For SAFE_1_1_ADDRESS, connect the Protocol Kit to the inner Safe and call signMessage with SigningMethod.SAFE_SIGNATURE, passing the parent Safe address:
let messageSafe1_1 = await createMessage(TYPED_MESSAGE)
protocolKit = await protocolKit.connect({
provider: RPC_URL,
signer: OWNER_3_PRIVATE_KEY,
safeAddress: SAFE_1_1_ADDRESS
})
messageSafe1_1 = await signMessage(
messageSafe1_1,
SigningMethod.SAFE_SIGNATURE,
SAFE_3_4_ADDRESS // Parent Safe address
)
The parentSafeAddress argument is critical: it tells the nested Safe which parent’s EIP-712 domain its signature must bind to. The inner Safe’s owner signs the message against SAFE_3_4_ADDRESS’s domain, not against SAFE_1_1_ADDRESS’s own domain. Without this, the outer checkSignatures would compute a different digest and reject the signature.
For SAFE_2_3_ADDRESS, the same pattern repeats with two different owners signing, because the inner Safe needs two signatures to meet its own 2-of-3 threshold:
// OWNER_4 signs
messageSafe2_3 = await signMessage(messageSafe2_3, SigningMethod.SAFE_SIGNATURE, SAFE_3_4_ADDRESS)
// OWNER_5 signs
messageSafe2_3 = await signMessage(messageSafe2_3, SigningMethod.SAFE_SIGNATURE, SAFE_3_4_ADDRESS)
Each EthSafeMessage instance (messageSafe1_1, messageSafe2_3) is an independent accumulator collecting signatures on behalf of its respective Safe.
Step 2: Build each nested Safe’s contract signature
Once an inner Safe has enough signatures to meet its own threshold, wrap them into a single contract signature:
const signatureSafe1_1 = await buildContractSignature(
Array.from(messageSafe1_1.signatures.values()),
SAFE_1_1_ADDRESS
)
const signatureSafe2_3 = await buildContractSignature(
Array.from(messageSafe2_3.signatures.values()),
SAFE_2_3_ADDRESS
)
buildContractSignature(signatures, signerAddress) produces a SafeSignature with:
signer= the nested Safe’s addressdata= the encoded concatenation of the nested Safe’s own owner signaturesisContractSignature=truestaticPart= 65 bytes withv = 0,r = signerAddress,s = offset to dynamic data(Safe’s discriminator for contract signatures)
The resulting bytes match exactly what Safe’s checkSignatures expects: when it sees v = 0 during parsing, it interprets r as an address, looks at the dynamic tail to find the contract-signature bytes, and calls IERC1271(r).isValidSignature(txHash, signatureBytes). The EIP-1271 call returns the magic value if the nested Safe’s own checkSignatures is satisfied.
Step 3: Add the contract signatures to the outer Safe’s accumulator
Connect the Protocol Kit to SAFE_3_4_ADDRESS, collect the EOA signatures from OWNER_1_ADDRESS and OWNER_2_ADDRESS, and then add the two nested-Safe contract signatures:
safeMessage.addSignature(signatureSafe1_1)
safeMessage.addSignature(signatureSafe2_3)
The parent EthSafeMessage.signatures map now contains four SafeSignature entries — two ECDSA, two contract — keyed by owner address. At validation time, encodedSignatures() serializes all four into the concatenated bytes blob Safe expects, and the on-chain checkSignatures resolves each entry: ECDSA via ecrecover, contract via a recursive isValidSignature call.
The recursion
Nothing stops a nested Safe from itself being owned by other Safes. Each level of nesting adds one buildContractSignature layer: the innermost Safes collect their owners’ ECDSA signatures, wrap them into contract signatures, and hand them up to the level above. The level above treats the wrapped contract signatures as opaque — it only knows “this is a signature from this address, and isValidSignature says it’s valid”. The recursion unwinds at each level through the on-chain checkSignatures implementation.
Each recursive EIP-1271 call is a real staticcall into the nested Safe’s CompatibilityFallbackHandler. Gas costs accumulate with nesting depth, but the interface remains the same at every level: isValidSignature(hash, signature) returns the magic value 0x1626ba7e iff the nested Safe is satisfied.
Why this matters
Multisig-of-multisig is how organizations build governance hierarchies on Ethereum. A DAO’s treasury Safe might be owned by a board Safe and a reserve Safe; the board Safe might be owned by individual signer Safes. Without nested contract signatures, every action would require the lowest-level EOAs to sign directly for the top-level Safe, and the middle-layer Safes would have no role in the signing process.
With nested contract signatures, each Safe’s own threshold logic is enforced at its own level. The board Safe satisfies its threshold independently of the treasury Safe, the reserve Safe satisfies its threshold independently of the board Safe, and the treasury Safe only sees final contract signatures that already encode those consensus decisions. The Protocol Kit makes this work by abstracting the entire recursion behind SigningMethod.SAFE_SIGNATURE and buildContractSignature.
Connections
- Safe Protocol Kit: the SDK that provides
buildContractSignature - Safe Protocol Kit Message Signatures: the full flow including nested signing
- EthSafeMessage: the accumulator each nested Safe uses
- Safe SigningMethod: the
SAFE_SIGNATUREenum value that triggers nested signing - EIP-1271: Standard Signature Validation Method for Contracts: the interface used at every level of recursion
- Smart Account Signatures: the broader EOA vs contract signer pattern