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 │ │Execute │ Transaction │ │
│ │ └────────┘ └────────┘ └────────┘ │ │
│ │ │ │
│ │ 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
| Category | Recommendation |
|---|---|
| Threshold | Use (n/2)+1 or higher for critical operations |
| Key management | Store keys in hardware wallets or HSMs |
| Backup | Have clear backup/recovery procedures |
| Monitoring | Alert on all proposal creations |
| Timeouts | Set expiration on proposals |
| Separation | Different multisigs for different purposes |
Next: Project Examples - Complete project implementations.