6. Multi Signature Flow

Walk through the full lifecycle of a multi-owner Safe: add an owner, change the threshold, propose with one signer, confirm with another, and execute once the threshold is met.

What you'll learn

  • Add and remove owners from a Safe programmatically

  • Change the signing threshold

  • Propose a transaction signed by one owner, then confirm with a second

  • Execute a transaction that requires multiple signatures

  • Handle Transaction Service indexing lag in multi-step flows

Prerequisites

  • Node.js 18+

  • A Safe on Sepolia where you control an owner key (starting as 1-of-1)

  • A private key for the existing Safe owner: testnet only

  • Sepolia ETH in the Safe for gas (multiple on-chain transactions)

  • viem: used for address derivation and transaction receipts

Install dependencies:

npm install @safe-global/api-kit @safe-global/protocol-kit @safe-global/types-kit viem

Configuration

The TX_SERVICE_URL points at the Ledger-hosted Transaction Service. Transactions proposed here appear in the Ledger Multisig UIarrow-up-right.

Supported chains: Ethereum (1), Optimism (10), BSC (56), Polygon (137), Base (8453), Arbitrum (42161), Sepolia (11155111).

Helper: wait for receipt

Throughout this tutorial, we verify every on-chain transaction. This helper waits for the receipt and throws if the transaction reverted:

Helper: reinitialize Protocol Kit

After any on-chain state change (new owner, new threshold), reinitialize the Protocol Kit to pick up the latest Safe state:

Helper: normalize private keys

Use one key format consistently. This helper accepts either "abc..." or "0xabc...":

Step-by-step

1

Add a second owner

Starting from a 1-of-1 Safe, generate an ephemeral Owner B key, then add Owner B while keeping the threshold at 1 (so Owner A can still execute alone for now).

What happens on-chain: The Safe calls its own addOwnerWithThreshold(owner, threshold) method, adding Owner B to the owner list.

Verify the new owner was added:

2

Change threshold to 2

Now require both owners to sign. This is an on-chain Safe transaction:

Critical: After changing the threshold on-chain, you must wait for the Transaction Service to index the new state before proposing new transactions. Otherwise, confirmationsRequired in the API response will still show the old threshold.

3

Propose a transaction (Owner A signs)

With threshold = 2, a single signature is no longer enough to execute. Owner A proposes and signs:

REST API: POST /v1/safes/{address}/multisig-transactions/

4

Confirm with Owner B

Owner B retrieves the pending transaction and adds their confirmation (off-chain signature):

REST API: POST /v1/multisig-transactions/{safeTxHash}/confirmations/

5

Execute (threshold met)

Either owner (or any account) can now execute the transaction, since both signatures are collected in the Transaction Service:

The Protocol Kit pulls the collected signatures from the transaction object and submits them together in the on-chain execution call.

6

Reset (optional): remove owner and lower threshold

To return to a 1-of-1 configuration (e.g., after testing), remove Owner B and lower the threshold in a single transaction. This still requires both signatures since threshold is currently 2:

Key concepts

How threshold signing works:

A Safe with N owners and threshold T requires at least T unique owner signatures before a transaction can execute.

Phase
Who
What happens
Gas?

Propose

Any owner or delegate

Transaction data + first signature submitted to TX Service

No

Confirm

Other owners

Additional signatures submitted to TX Service

No

Execute

Anyone (usually an owner)

All signatures bundled and submitted on-chain

Yes

Signatures are collected off-chain (free) and only the final execution costs gas. This is the core efficiency of the Safe signature model.

For the full guide, see Transactions with Off-chain Signaturesarrow-up-right.

Tips and pitfalls

  • Transaction Service indexing lag is critical here. After any on-chain state change (adding an owner, changing threshold), the Transaction Service needs time (10–60 seconds) to index the new state. If you immediately propose a new transaction, the API may report the old confirmationsRequired value. Always poll getSafeInfo until it reflects the expected threshold before proceeding.

  • Always verify receipt status. executeTransaction returns a hash even if the on-chain transaction reverts. Use waitForTransactionReceipt and check receipt.status:

  • Reinitialize the Protocol Kit after state changes. The Protocol Kit caches the Safe's owner list and threshold at initialization. After adding/removing owners or changing the threshold, call Safe.init(...) again to pick up the new state.

  • Private key format must be consistent. Avoid mixing prefixed/non-prefixed keys manually (for example 0x + 0x...). Use a key normalizer helper and keep the format uniform.

  • Signature ordering. The Safe contract expects signatures sorted by signer address (ascending, lowercase). The Protocol Kit and Transaction Service handle this automatically when you use executeTransaction with a transaction object from getTransaction. Don't manually reorder signatures.

  • createRemoveOwnerTx with threshold. When removing an owner, you can simultaneously lower the threshold. If you set the threshold higher than the remaining owner count, the transaction will revert.

  • Don't lose keys mid-flow. If you change the threshold to 2-of-2 and then lose access to one key, you'll be locked out of the Safe permanently. In production, always test threshold changes carefully and keep backup access plans.

Last updated