Multisig Operations

Multisig wallets require multiple signatures to execute transactions, providing enhanced security for program administration and treasury management.

Multisig Architecture

Text
┌─────────────────────────────────────────────────────────────────┐
│                    Multisig Architecture                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Multisig Account                                               │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ Address: Multi123...                                     │   │
│  │ Threshold: 2 of 3                                        │   │
│  │                                                          │   │
│  │ Owners:                                                  │   │
│  │  ├── Owner1: Abc123...                                  │   │
│  │  ├── Owner2: Def456...                                  │   │
│  │  └── Owner3: Ghi789...                                  │   │
│  │                                                          │   │
│  │ Transaction Queue: [Tx1, Tx2]                           │   │
│  └─────────────────────────────────────────────────────────┘   │
│                           │                                     │
│                           ▼                                     │
│  Transaction Flow                                               │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                                                          │   │
│  │  1. Propose    2. Approve     3. Execute                │   │
│  │  ┌────────┐   ┌────────┐    ┌────────┐                 │   │
│  │  │ Owner1 │──▶│ Owner2 │───▶│ Anyone │──▶ Execute     │   │
│  │  │Propose │   │Approve │    │ExecuteTransaction   │   │
│  │  └────────┘   └────────┘    └────────┘                 │   │
│  │                                                          │   │
│  │  Signatures: [Owner1,Owner2, _ Owner3]            │   │
│  │  Status: Ready to execute (2/3 threshold met)           │   │
│  │                                                          │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Squads Multisig Integration

TypeScript
import {
  Connection,
  Keypair,
  PublicKey,
  Transaction,
  TransactionInstruction,
  SystemProgram,
} from "@solana/web3.js";
import Squads, {
  getAuthorityPDA,
  getMsPDA,
  getTxPDA,
  Permissions,
} from "@sqds/sdk";
import BN from "bn.js";

const SQUADS_PROGRAM_ID = new PublicKey(
  "SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf",
);

// Initialize Squads SDK
export async function initSquads(
  connection: Connection,
  wallet: Keypair,
): Promise<Squads> {
  return Squads.localnet(connection, wallet);
}

// Create a new multisig
export async function createMultisig(
  squads: Squads,
  threshold: number,
  members: PublicKey[],
  name: string,
): Promise<{
  multisigAddress: PublicKey;
  vaultAddress: PublicKey;
}> {
  // Create the multisig with members
  const createKey = Keypair.generate();

  const multisigAddress = await squads.createMultisig(
    threshold,
    createKey.publicKey,
    members.map((member) => ({
      key: member,
      permissions: Permissions.all(), // Full permissions
    })),
    name,
    "", // Description
  );

  // Get the vault PDA (index 0)
  const [vaultAddress] = getAuthorityPDA(
    multisigAddress,
    new BN(0),
    SQUADS_PROGRAM_ID,
  );

  return {
    multisigAddress,
    vaultAddress,
  };
}

// Create a transaction proposal
export async function proposeTransaction(
  squads: Squads,
  multisigAddress: PublicKey,
  instructions: TransactionInstruction[],
): Promise<{
  transactionAddress: PublicKey;
  transactionIndex: number;
}> {
  // Get next transaction index
  const msAccount = await squads.getMultisig(multisigAddress);
  const transactionIndex = msAccount.transactionIndex + 1;

  // Create transaction
  const transactionAddress = await squads.createTransaction(
    multisigAddress,
    1, // Authority index (vault)
  );

  // Add instructions
  for (let i = 0; i < instructions.length; i++) {
    await squads.addInstruction(transactionAddress, instructions[i]);
  }

  // Activate the transaction (ready for voting)
  await squads.activateTransaction(transactionAddress);

  return {
    transactionAddress,
    transactionIndex,
  };
}

// Approve a transaction
export async function approveTransaction(
  squads: Squads,
  transactionAddress: PublicKey,
): Promise<string> {
  const signature = await squads.approveTransaction(transactionAddress);
  return signature;
}

// Execute when threshold is met
export async function executeTransaction(
  squads: Squads,
  transactionAddress: PublicKey,
): Promise<string> {
  const signature = await squads.executeTransaction(transactionAddress);
  return signature;
}

// Reject a transaction
export async function rejectTransaction(
  squads: Squads,
  transactionAddress: PublicKey,
): Promise<string> {
  const signature = await squads.rejectTransaction(transactionAddress);
  return signature;
}

// Example: Program upgrade via multisig
export async function proposeUpgrade(
  squads: Squads,
  multisigAddress: PublicKey,
  programId: PublicKey,
  bufferAddress: PublicKey,
): Promise<PublicKey> {
  const [vaultAddress] = getAuthorityPDA(
    multisigAddress,
    new BN(0),
    SQUADS_PROGRAM_ID,
  );

  const [programDataAddress] = PublicKey.findProgramAddressSync(
    [programId.toBuffer()],
    new PublicKey("BPFLoaderUpgradeab1e11111111111111111111111"),
  );

  // Create upgrade instruction
  const upgradeIx: TransactionInstruction = {
    programId: new PublicKey("BPFLoaderUpgradeab1e11111111111111111111111"),
    keys: [
      { pubkey: programDataAddress, isSigner: false, isWritable: true },
      { pubkey: programId, isSigner: false, isWritable: true },
      { pubkey: bufferAddress, isSigner: false, isWritable: true },
      { pubkey: vaultAddress, isSigner: false, isWritable: true }, // Spill
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
      { pubkey: vaultAddress, isSigner: true, isWritable: false }, // Authority
    ],
    data: Buffer.from([3]), // Upgrade instruction
  };

  const { transactionAddress } = await proposeTransaction(
    squads,
    multisigAddress,
    [upgradeIx],
  );

  return transactionAddress;
}

Custom Multisig Program

Rust
use anchor_lang::prelude::*;

declare_id!("Multi11111111111111111111111111111111111111");

#[account]
pub struct Multisig {
    pub threshold: u8,
    pub owners: Vec<Pubkey>,
    pub transaction_count: u64,
    pub bump: u8,
}

impl Multisig {
    pub const MAX_OWNERS: usize = 10;
    pub const SPACE: usize = 8 + 1 + 4 + (32 * Self::MAX_OWNERS) + 8 + 1;

    pub fn is_owner(&self, key: &Pubkey) -> bool {
        self.owners.contains(key)
    }
}

#[account]
pub struct Transaction {
    pub multisig: Pubkey,
    pub index: u64,
    pub proposer: Pubkey,
    pub instructions: Vec<ProposedInstruction>,
    pub signers: Vec<bool>,
    pub executed: bool,
    pub created_at: i64,
    pub bump: u8,
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct ProposedInstruction {
    pub program_id: Pubkey,
    pub keys: Vec<AccountMetaData>,
    pub data: Vec<u8>,
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct AccountMetaData {
    pub pubkey: Pubkey,
    pub is_signer: bool,
    pub is_writable: bool,
}

#[program]
pub mod multisig {
    use super::*;

    pub fn create_multisig(
        ctx: Context<CreateMultisig>,
        threshold: u8,
        owners: Vec<Pubkey>,
    ) -> Result<()> {
        require!(threshold > 0, ErrorCode::InvalidThreshold);
        require!(
            (threshold as usize) <= owners.len(),
            ErrorCode::InvalidThreshold
        );
        require!(owners.len() <= Multisig::MAX_OWNERS, ErrorCode::TooManyOwners);

        // Check for duplicates
        let mut sorted_owners = owners.clone();
        sorted_owners.sort();
        for i in 1..sorted_owners.len() {
            require!(
                sorted_owners[i] != sorted_owners[i - 1],
                ErrorCode::DuplicateOwner
            );
        }

        let multisig = &mut ctx.accounts.multisig;
        multisig.threshold = threshold;
        multisig.owners = owners;
        multisig.transaction_count = 0;
        multisig.bump = ctx.bumps.multisig;

        Ok(())
    }

    pub fn create_transaction(
        ctx: Context<CreateTransaction>,
        instructions: Vec<ProposedInstruction>,
    ) -> Result<()> {
        let multisig = &mut ctx.accounts.multisig;
        let transaction = &mut ctx.accounts.transaction;
        let proposer = &ctx.accounts.proposer;

        require!(multisig.is_owner(&proposer.key()), ErrorCode::NotAnOwner);

        // Initialize transaction
        transaction.multisig = multisig.key();
        transaction.index = multisig.transaction_count;
        transaction.proposer = proposer.key();
        transaction.instructions = instructions;
        transaction.signers = vec![false; multisig.owners.len()];
        transaction.executed = false;
        transaction.created_at = Clock::get()?.unix_timestamp;
        transaction.bump = ctx.bumps.transaction;

        // Auto-approve by proposer
        let owner_index = multisig
            .owners
            .iter()
            .position(|o| o == &proposer.key())
            .unwrap();
        transaction.signers[owner_index] = true;

        multisig.transaction_count += 1;

        emit!(TransactionCreated {
            multisig: multisig.key(),
            transaction: transaction.key(),
            index: transaction.index,
            proposer: proposer.key(),
        });

        Ok(())
    }

    pub fn approve(ctx: Context<Approve>) -> Result<()> {
        let multisig = &ctx.accounts.multisig;
        let transaction = &mut ctx.accounts.transaction;
        let approver = &ctx.accounts.approver;

        require!(multisig.is_owner(&approver.key()), ErrorCode::NotAnOwner);
        require!(!transaction.executed, ErrorCode::AlreadyExecuted);

        let owner_index = multisig
            .owners
            .iter()
            .position(|o| o == &approver.key())
            .unwrap();

        require!(!transaction.signers[owner_index], ErrorCode::AlreadySigned);

        transaction.signers[owner_index] = true;

        emit!(TransactionApproved {
            multisig: multisig.key(),
            transaction: transaction.key(),
            approver: approver.key(),
        });

        Ok(())
    }

    pub fn execute<'info>(ctx: Context<'_, '_, '_, 'info, Execute<'info>>) -> Result<()> {
        let multisig = &ctx.accounts.multisig;
        let transaction = &mut ctx.accounts.transaction;

        require!(!transaction.executed, ErrorCode::AlreadyExecuted);

        // Count approvals
        let approval_count = transaction.signers.iter().filter(|&s| *s).count();
        require!(
            approval_count >= multisig.threshold as usize,
            ErrorCode::ThresholdNotMet
        );

        // Execute each instruction
        let multisig_key = multisig.key();
        let seeds = &[b"multisig", multisig_key.as_ref(), &[multisig.bump]];
        let signer_seeds = &[&seeds[..]];

        for ix in &transaction.instructions {
            let mut accounts = Vec::new();
            for meta in &ix.keys {
                accounts.push(if meta.is_writable {
                    AccountMeta::new(meta.pubkey, meta.is_signer)
                } else {
                    AccountMeta::new_readonly(meta.pubkey, meta.is_signer)
                });
            }

            let instruction = anchor_lang::solana_program::instruction::Instruction {
                program_id: ix.program_id,
                accounts,
                data: ix.data.clone(),
            };

            anchor_lang::solana_program::program::invoke_signed(
                &instruction,
                ctx.remaining_accounts,
                signer_seeds,
            )?;
        }

        transaction.executed = true;

        emit!(TransactionExecuted {
            multisig: multisig.key(),
            transaction: transaction.key(),
            executor: ctx.accounts.executor.key(),
        });

        Ok(())
    }

    pub fn change_threshold(ctx: Context<ChangeConfig>, new_threshold: u8) -> Result<()> {
        let multisig = &mut ctx.accounts.multisig;

        require!(
            (new_threshold as usize) <= multisig.owners.len(),
            ErrorCode::InvalidThreshold
        );
        require!(new_threshold > 0, ErrorCode::InvalidThreshold);

        multisig.threshold = new_threshold;

        Ok(())
    }

    pub fn add_owner(ctx: Context<ChangeConfig>, new_owner: Pubkey) -> Result<()> {
        let multisig = &mut ctx.accounts.multisig;

        require!(
            multisig.owners.len() < Multisig::MAX_OWNERS,
            ErrorCode::TooManyOwners
        );
        require!(!multisig.is_owner(&new_owner), ErrorCode::DuplicateOwner);

        multisig.owners.push(new_owner);

        Ok(())
    }

    pub fn remove_owner(ctx: Context<ChangeConfig>, owner_to_remove: Pubkey) -> Result<()> {
        let multisig = &mut ctx.accounts.multisig;

        require!(multisig.is_owner(&owner_to_remove), ErrorCode::NotAnOwner);
        require!(
            multisig.owners.len() > multisig.threshold as usize,
            ErrorCode::InvalidThreshold
        );

        multisig.owners.retain(|o| o != &owner_to_remove);

        Ok(())
    }
}

#[derive(Accounts)]
pub struct CreateMultisig<'info> {
    #[account(
        init,
        payer = payer,
        space = Multisig::SPACE,
        seeds = [b"multisig", payer.key().as_ref()],
        bump
    )]
    pub multisig: Account<'info, Multisig>,

    #[account(mut)]
    pub payer: Signer<'info>,

    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct CreateTransaction<'info> {
    #[account(mut)]
    pub multisig: Account<'info, Multisig>,

    #[account(
        init,
        payer = proposer,
        space = 8 + 32 + 8 + 32 + 4 + 1000 + 4 + 10 + 1 + 8 + 1, // Approximate
        seeds = [
            b"transaction",
            multisig.key().as_ref(),
            &multisig.transaction_count.to_le_bytes()
        ],
        bump
    )]
    pub transaction: Account<'info, Transaction>,

    #[account(mut)]
    pub proposer: Signer<'info>,

    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Approve<'info> {
    pub multisig: Account<'info, Multisig>,

    #[account(
        mut,
        constraint = transaction.multisig == multisig.key()
    )]
    pub transaction: Account<'info, Transaction>,

    pub approver: Signer<'info>,
}

#[derive(Accounts)]
pub struct Execute<'info> {
    #[account(
        seeds = [b"multisig", multisig.key().as_ref()],
        bump = multisig.bump
    )]
    pub multisig: Account<'info, Multisig>,

    #[account(
        mut,
        constraint = transaction.multisig == multisig.key()
    )]
    pub transaction: Account<'info, Transaction>,

    pub executor: Signer<'info>,
}

#[derive(Accounts)]
pub struct ChangeConfig<'info> {
    #[account(
        mut,
        seeds = [b"multisig", multisig.key().as_ref()],
        bump = multisig.bump
    )]
    pub multisig: Account<'info, Multisig>,

    // Must be called via execute (multisig is signer)
}

#[event]
pub struct TransactionCreated {
    pub multisig: Pubkey,
    pub transaction: Pubkey,
    pub index: u64,
    pub proposer: Pubkey,
}

#[event]
pub struct TransactionApproved {
    pub multisig: Pubkey,
    pub transaction: Pubkey,
    pub approver: Pubkey,
}

#[event]
pub struct TransactionExecuted {
    pub multisig: Pubkey,
    pub transaction: Pubkey,
    pub executor: Pubkey,
}

#[error_code]
pub enum ErrorCode {
    #[msg("Invalid threshold")]
    InvalidThreshold,
    #[msg("Too many owners")]
    TooManyOwners,
    #[msg("Duplicate owner")]
    DuplicateOwner,
    #[msg("Not an owner")]
    NotAnOwner,
    #[msg("Already signed")]
    AlreadySigned,
    #[msg("Already executed")]
    AlreadyExecuted,
    #[msg("Threshold not met")]
    ThresholdNotMet,
}

TypeScript Client

TypeScript
import {
  Connection,
  Keypair,
  PublicKey,
  Transaction,
  TransactionInstruction,
  SystemProgram,
} from "@solana/web3.js";
import * as anchor from "@coral-xyz/anchor";

class MultisigClient {
  private program: anchor.Program;
  private connection: Connection;

  constructor(program: anchor.Program) {
    this.program = program;
    this.connection = program.provider.connection;
  }

  // Get multisig PDA
  getMultisigAddress(creator: PublicKey): [PublicKey, number] {
    return PublicKey.findProgramAddressSync(
      [Buffer.from("multisig"), creator.toBuffer()],
      this.program.programId,
    );
  }

  // Get transaction PDA
  getTransactionAddress(
    multisig: PublicKey,
    index: number,
  ): [PublicKey, number] {
    return PublicKey.findProgramAddressSync(
      [
        Buffer.from("transaction"),
        multisig.toBuffer(),
        new anchor.BN(index).toArrayLike(Buffer, "le", 8),
      ],
      this.program.programId,
    );
  }

  async createMultisig(
    payer: Keypair,
    threshold: number,
    owners: PublicKey[],
  ): Promise<PublicKey> {
    const [multisigAddress] = this.getMultisigAddress(payer.publicKey);

    await this.program.methods
      .createMultisig(threshold, owners)
      .accounts({
        multisig: multisigAddress,
        payer: payer.publicKey,
        systemProgram: SystemProgram.programId,
      })
      .signers([payer])
      .rpc();

    return multisigAddress;
  }

  async proposeTransaction(
    multisig: PublicKey,
    proposer: Keypair,
    instructions: TransactionInstruction[],
  ): Promise<PublicKey> {
    const multisigAccount = await this.program.account.multisig.fetch(multisig);
    const index = multisigAccount.transactionCount.toNumber();

    const [transactionAddress] = this.getTransactionAddress(multisig, index);

    const proposedInstructions = instructions.map((ix) => ({
      programId: ix.programId,
      keys: ix.keys.map((key) => ({
        pubkey: key.pubkey,
        isSigner: key.isSigner,
        isWritable: key.isWritable,
      })),
      data: Buffer.from(ix.data),
    }));

    await this.program.methods
      .createTransaction(proposedInstructions)
      .accounts({
        multisig,
        transaction: transactionAddress,
        proposer: proposer.publicKey,
        systemProgram: SystemProgram.programId,
      })
      .signers([proposer])
      .rpc();

    return transactionAddress;
  }

  async approve(
    multisig: PublicKey,
    transactionAddress: PublicKey,
    approver: Keypair,
  ): Promise<string> {
    return await this.program.methods
      .approve()
      .accounts({
        multisig,
        transaction: transactionAddress,
        approver: approver.publicKey,
      })
      .signers([approver])
      .rpc();
  }

  async execute(
    multisig: PublicKey,
    transactionAddress: PublicKey,
    executor: Keypair,
  ): Promise<string> {
    const transaction =
      await this.program.account.transaction.fetch(transactionAddress);

    // Collect all accounts needed for execution
    const remainingAccounts: anchor.web3.AccountMeta[] = [];
    for (const ix of transaction.instructions) {
      for (const key of ix.keys) {
        remainingAccounts.push({
          pubkey: key.pubkey,
          isSigner: key.isSigner,
          isWritable: key.isWritable,
        });
      }
      // Add program ID as read-only
      remainingAccounts.push({
        pubkey: ix.programId,
        isSigner: false,
        isWritable: false,
      });
    }

    return await this.program.methods
      .execute()
      .accounts({
        multisig,
        transaction: transactionAddress,
        executor: executor.publicKey,
      })
      .remainingAccounts(remainingAccounts)
      .signers([executor])
      .rpc();
  }

  async getMultisigInfo(multisig: PublicKey) {
    const account = await this.program.account.multisig.fetch(multisig);
    return {
      threshold: account.threshold,
      owners: account.owners,
      transactionCount: account.transactionCount.toNumber(),
    };
  }

  async getPendingTransactions(multisig: PublicKey) {
    const transactions = await this.program.account.transaction.all([
      {
        memcmp: {
          offset: 8, // After discriminator
          bytes: multisig.toBase58(),
        },
      },
    ]);

    return transactions
      .filter((t) => !t.account.executed)
      .map((t) => ({
        address: t.publicKey,
        index: t.account.index.toNumber(),
        proposer: t.account.proposer,
        approvals: t.account.signers.filter(Boolean).length,
        createdAt: new Date(t.account.createdAt.toNumber() * 1000),
      }));
  }
}

Best Practices

CategoryRecommendation
ThresholdUse (n/2)+1 or higher for critical operations
Key managementStore keys in hardware wallets or HSMs
BackupHave clear backup/recovery procedures
MonitoringAlert on all proposal creations
TimeoutsSet expiration on proposals
SeparationDifferent multisigs for different purposes

Next: Project Examples - Complete project implementations.