Skip to main content

Multisig Smart Accounts with Safe

Learn how to create and manage multisig Safe accounts with multiple owners, threshold signing, and dynamic owner management for secure shared control.

If you need help with the basics, check out the Getting Started Guide.

Quickstart

You can also fork the complete code and follow along.

Create Multisig Account

Set up a multisig account with multiple owners and signature threshold:

Create multisig account
import { SafeAccountV0_3_0 as SafeAccount } from "abstractionkit";

const ownerPublicAddress1 = process.env.PUBLIC_ADDRESS1 as string;
const ownerPublicAddress2 = process.env.PUBLIC_ADDRESS2 as string;

// Create a 2/2 multisig account (requires both signatures)
const smartAccount = SafeAccount.initializeNewAccount(
[ownerPublicAddress1, ownerPublicAddress2],
{ threshold: 2 }
);

console.log("Multisig Account Address:", smartAccount.accountAddress);

Learn more: initializeNewAccount

Fund Your Account

Make sure to fund your multisig account with ETH before sending transactions, or use a paymaster for gas sponsorship.

Sign with Multiple Keys

Sign UserOperations with all required private keys:

Multisig signing
const chainId = BigInt(process.env.CHAIN_ID as string);
const privateKey1 = process.env.PRIVATE_KEY1 as string;
const privateKey2 = process.env.PRIVATE_KEY2 as string;

// Sign with both private keys for 2/2 multisig
userOperation.signature = smartAccount.signUserOperation(
userOperation,
[privateKey1, privateKey2], // Array of all required signatures
chainId
);

Learn more: signUserOperation

Complete Runnable Example

Below is a complete example that demonstrates multisig account creation and signing:

Full Working Example
import * as dotenv from 'dotenv'

import {
SafeAccountV0_3_0 as SafeAccount,
MetaTransaction,
getFunctionSelector,
createCallData,
} from "abstractionkit";

async function main(): Promise<void> {
// Load environment variables
dotenv.config()
const chainId = BigInt(process.env.CHAIN_ID as string)
const bundlerUrl = process.env.BUNDLER_URL as string
const jsonRpcNodeProvider = process.env.JSON_RPC_NODE_PROVIDER as string
const ownerPublicAddress1 = process.env.PUBLIC_ADDRESS1 as string
const ownerPrivateKey1 = process.env.PRIVATE_KEY1 as string
const ownerPublicAddress2 = process.env.PUBLIC_ADDRESS2 as string
const ownerPrivateKey2 = process.env.PRIVATE_KEY2 as string

// Create 2/2 multisig account
const smartAccount = SafeAccount.initializeNewAccount(
[ownerPublicAddress1, ownerPublicAddress2],
{ threshold: 2 }
)

console.log("🔐 Multisig Account Address:", smartAccount.accountAddress)

// Create example transactions (minting NFTs)
const nftContractAddress = "0x9a7af758aE5d7B6aAE84fe4C5Ba67c041dFE5336";
const mintFunctionSignature = 'mint(address)';
const mintFunctionSelector = getFunctionSelector(mintFunctionSignature);
const mintTransactionCallData = createCallData(
mintFunctionSelector,
["address"],
[smartAccount.accountAddress]
);

const transaction1: MetaTransaction = {
to: nftContractAddress,
value: 0n,
data: mintTransactionCallData,
}

const transaction2: MetaTransaction = {
to: nftContractAddress,
value: 0n,
data: mintTransactionCallData,
}

// Create UserOperation
let userOperation = await smartAccount.createUserOperation(
[transaction1, transaction2],
jsonRpcNodeProvider,
bundlerUrl
)

console.log("📋 UserOperation created, awaiting multisig signatures...")

// Sign with both private keys (2/2 multisig)
userOperation.signature = smartAccount.signUserOperation(
userOperation,
[ownerPrivateKey1, ownerPrivateKey2], // All required signatures
chainId
)

console.log("✍️ UserOperation signed by all parties")

// Submit the multisig transaction
const sendUserOperationResponse = await smartAccount.sendUserOperation(
userOperation,
bundlerUrl
)

console.log("📤 Multisig UserOperation sent. Waiting for confirmation...")

// Wait for transaction to be included
let userOperationReceiptResult = await sendUserOperationResponse.included()

console.log("📋 UserOperation receipt received.")
console.log(userOperationReceiptResult)

if (userOperationReceiptResult.success) {
console.log("🎉 Multisig transaction successful! Hash:", userOperationReceiptResult.receipt.transactionHash)
} else {
console.log("❌ UserOperation execution failed")
}
}

main().catch(console.error)

Learn more: initializeNewAccount | createUserOperation | signUserOperation | sendUserOperation

Managing Multisig Owners

After creating a multisig account, you can dynamically add, remove, or swap owners:

Add a new owner
import { SafeAccountV0_3_0 as SafeAccount } from "abstractionkit";

// Add a new owner with threshold of 3 (now 3/3 multisig)
const newOwnerAddress = "0x..."; // New owner's address
const newThreshold = 3;

const addOwnerTx = await smartAccount.createAddOwnerWithThresholdMetaTransactions(
newOwnerAddress,
newThreshold,
jsonRpcNodeProvider
);

// Create UserOperation with the add owner transaction
let userOperation = await smartAccount.createUserOperation(
[addOwnerTx],
jsonRpcNodeProvider,
bundlerUrl
);

// Sign with existing owners (2/2 required)
userOperation.signature = smartAccount.signUserOperation(
userOperation,
[privateKey1, privateKey2],
chainId
);

Learn more: createAddOwnerWithThresholdMetaTransactions

Multisig Configurations

Different threshold configurations serve various real-world scenarios:

2/2 - Maximum Security

  • Two-Factor Authentication: Phone wallet + hardware wallet / second device for personal accounts
  • Business Partnership: Joint business account where both partners must approve all transactions
  • High-Value Storage: Maximum security for large amounts
Account Recovery Required

2/2 setups can lock owners out permanently if they lose any key. Always implement Account Recovery.

2/3 - Balanced Control

  • Company Treasury: CEO, CFO, CTO where any 2 can approve (business continuity)
  • Family Trust: Parents + adult child with built-in inheritance planning
  • Small Team: Flexible governance with backup access

3/5+ - Distributed Governance

  • DAO Treasury: Democratic council governance requiring majority consensus
  • Professional Custody: Institutional-grade security with distributed validators
  • Large Organizations: Democratic decision making with higher coordination overhead

1/N - Flexible Access

  • Team Operations: Shared operational funds for routine expenses
  • Development Teams: Independent spending for daily operations