# Multisig Smart Accounts with Safe

Create and manage multisig Safe accounts with multiple owners, threshold signing, and dynamic owner management for secure shared control.

> For the basics, see the [Getting Started Guide](https://docs.candide.dev/wallet/guides/getting-started.md).

[YouTube video player](https://www.youtube.com/embed/jZFx8cYRR48?si=EsS_DT5NRWyKDSaZ)

## Quickstart[​](#quickstart "Direct link to Quickstart")

> You can also [fork the complete code](https://github.com/candidelabs/abstractionkit-examples/blob/main/multisig/multisig.ts) and follow along.

### Create Multisig Account[​](#create-multisig-account "Direct link to Create Multisig Account")

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

* index.ts
* .env

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`](https://docs.candide.dev/wallet/abstractionkit/safe-account-v3/.md#initializenewaccount)

Fund Your Account

Make sure to fund your multisig account with ETH before sending transactions, or use a [paymaster for gas sponsorship](https://docs.candide.dev/wallet/guides/send-gasless-tx.md).

```
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-with-multiple-keys "Direct link to Sign with Multiple Keys")

Sign UserOperations with all required private keys:

* signing.ts
* Threshold Examples

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`](https://docs.candide.dev/wallet/abstractionkit/safe-account-v3/.md#signuseroperation)

Different threshold configurations

```
// 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`](https://docs.candide.dev/wallet/abstractionkit/safe-account-v3/.md#initializenewaccount) | [`signUserOperation`](https://docs.candide.dev/wallet/abstractionkit/safe-account-v3/.md#signuseroperation)

#### Complete Runnable Example[​](#complete-runnable-example "Direct link to 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`](https://docs.candide.dev/wallet/abstractionkit/safe-account-v3/.md#initializenewaccount) | [`createUserOperation`](https://docs.candide.dev/wallet/abstractionkit/safe-account-v3/.md#createuseroperation) | [`signUserOperation`](https://docs.candide.dev/wallet/abstractionkit/safe-account-v3/.md#signuseroperation) | [`sendUserOperation`](https://docs.candide.dev/wallet/abstractionkit/safe-account-v3/.md#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[​](#managing-multisig-owners "Direct link to Managing Multisig Owners")

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

* Add Owner
* Remove Owner
* Swap Owner

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`](https://docs.candide.dev/wallet/abstractionkit/safe-account-v3/.md#createaddownerwiththresholdmetatransactions)

Remove an owner

```
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`](https://docs.candide.dev/wallet/abstractionkit/safe-account-v3/.md#createremoveownermetatransaction)

Replace an owner

```
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`](https://docs.candide.dev/wallet/abstractionkit/safe-account-v3/.md#createswapownermetatransactions)

## Multisig Configurations[​](#multisig-configurations "Direct link to Multisig Configurations")

Different threshold configurations serve various real-world scenarios:

#### 2/2 - Maximum Security[​](#22---maximum-security "Direct link to 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](https://docs.candide.dev/wallet/plugins/recovery-with-guardians.md).

#### 2/3 - Balanced Control[​](#23---balanced-control "Direct link to 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[​](#35---distributed-governance "Direct link to 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[​](#1n---flexible-access "Direct link to 1/N - Flexible Access")

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