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:
- index.ts
- .env
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
Make sure to fund your multisig account with ETH before sending transactions, or use a paymaster for gas sponsorship.
CHAIN_ID=11155111
BUNDLER_URL=https://api.candide.dev/public/v3/sepolia
JSON_RPC_NODE_PROVIDER=https://ethereum-sepolia-rpc.publicnode.com
# Multisig owners
PUBLIC_ADDRESS1=
PRIVATE_KEY1=
PUBLIC_ADDRESS2=
PRIVATE_KEY2=
Sign with Multiple Keys
Sign UserOperations with all required private keys:
- signing.ts
- Threshold Examples
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
// 1 of 2 multisig (only one signature required)
const smartAccount_1of2 = SafeAccount.initializeNewAccount(
[ownerAddress1, ownerAddress2],
{ threshold: 1 }
);
// 2 of 3 multisig (two signatures required)
const smartAccount_2of3 = SafeAccount.initializeNewAccount(
[ownerAddress1, ownerAddress2, ownerAddress3],
{ threshold: 2 }
);
// For 2/3 multisig, sign with any 2 owners:
userOperation.signature = smartAccount.signUserOperation(
userOperation,
[privateKey1, privateKey3], // Only 2 out of 3 needed
chainId
);
Learn more:
initializeNewAccount
|signUserOperation
Complete Runnable Example
Below is a complete example that demonstrates multisig account creation and signing:
Full Working Example
- index.ts
- .env
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
CHAIN_ID=11155111
BUNDLER_URL=https://api.candide.dev/public/v3/sepolia
JSON_RPC_NODE_PROVIDER=https://ethereum-sepolia-rpc.publicnode.com
# Multisig owners
PUBLIC_ADDRESS1=your_first_public_address
PRIVATE_KEY1=your_first_private_key
PUBLIC_ADDRESS2=your_second_public_address
PRIVATE_KEY2=your_second_private_key
Managing Multisig Owners
After creating a multisig account, you can dynamically add, remove, or swap owners:
- Add Owner
- Remove Owner
- Swap 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
const ownerToRemove = "0x..."; // Owner address to remove
const newThreshold = 1; // New threshold after removal
const removeOwnerTx = await smartAccount.createRemoveOwnerMetaTransaction(
ownerToRemove,
newThreshold,
jsonRpcNodeProvider
);
// Create UserOperation with the remove owner transaction
let userOperation = await smartAccount.createUserOperation(
[removeOwnerTx],
jsonRpcNodeProvider,
bundlerUrl
);
// Sign with required owners (current threshold)
userOperation.signature = smartAccount.signUserOperation(
userOperation,
[privateKey1, privateKey2], // All current owners must sign
chainId
);
Learn more:
createRemoveOwnerMetaTransaction
const oldOwnerAddress = "0x..."; // Owner to replace
const newOwnerAddress = "0x..."; // New owner address
const swapOwnerTxs = await smartAccount.createSwapOwnerMetaTransactions(
oldOwnerAddress,
newOwnerAddress,
jsonRpcNodeProvider
);
// Note: This returns an array of transactions (might include deployment)
let userOperation = await smartAccount.createUserOperation(
swapOwnerTxs, // Array of meta-transactions
jsonRpcNodeProvider,
bundlerUrl
);
// Sign with existing owners
userOperation.signature = smartAccount.signUserOperation(
userOperation,
[privateKey1, privateKey2],
chainId
);
Learn more:
createSwapOwnerMetaTransactions
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
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