All articles
solanasquadsmultisigsecurity

Squads: how Solana's standard multisig actually works on-chain

Squads is the canonical Solana multisig — Multisig PDA, threshold approvals, vault sub-accounts, and TX proposals. Here's the on-chain shape and how to use it.

Squads is the Solana ecosystem's standard multisig. Almost every protocol upgrade authority, treasury, and shared signer on mainnet is held by a Squads multisig. The current program is Squads V4, deployed at SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf.

It's well-designed and surprisingly simple once you see the account shape. Here's the on-chain view.

The account model

Three account types do the work:

rust
// 1. The Multisig — the configuration account
pub struct Multisig {
    pub create_key:           Pubkey,    // seed for PDA derivation
    pub config_authority:     Pubkey,    // can change members/threshold (often the multisig itself)
    pub threshold:            u16,        // M-of-N approval threshold
    pub time_lock:            u32,        // seconds before approved tx can execute
    pub transaction_index:    u64,        // monotonic counter
    pub stale_transaction_index: u64,    // older proposals get invalidated past this
    pub rent_collector:       Option<Pubkey>,
    pub members:              Vec<Member>,  // each Member has Pubkey + Permissions
}

// 2. The Vault — a PDA the multisig signs for
//    Derived from [b"multisig", multisig.key().as_ref(), b"vault", &[vault_index]]
//    Holds SOL, tokens, NFTs, and is the actual on-chain "wallet"

// 3. The VaultTransaction — a pending proposal
pub struct VaultTransaction {
    pub multisig:            Pubkey,
    pub creator:             Pubkey,
    pub index:               u64,
    pub bump:                u8,
    pub vault_index:         u8,
    pub vault_bump:          u8,
    pub ephemeral_signer_bumps: Vec<u8>,
    pub message:             VaultTransactionMessage,  // the proposed Solana instructions
}

// 4. The Proposal — tracks who's approved/rejected
pub struct Proposal {
    pub multisig:           Pubkey,
    pub transaction_index:  u64,
    pub status:             ProposalStatus,  // Draft, Active, Approved, Rejected, Executing, Executed, Cancelled
    pub bump:               u8,
    pub approved:           Vec<Pubkey>,
    pub rejected:           Vec<Pubkey>,
    pub cancelled:          Vec<Pubkey>,
}

Multisigs are addressed by their create_key (an arbitrary pubkey chosen at creation, used as the seed). One multisig can have multiple vault sub-accounts indexed from 0 — typical pattern is "Treasury" (index 0) plus application-specific vaults.

Member permissions

rust
pub struct Member {
    pub key:         Pubkey,
    pub permissions: Permissions,  // bitmask: Initiate | Vote | Execute
}

pub mod Permission {
    pub const INITIATE: u8 = 1 << 0;  // create new transactions
    pub const VOTE:     u8 = 1 << 1;  // approve/reject
    pub const EXECUTE:  u8 = 1 << 2;  // run approved transactions
}

Members have fine-grained roles — you can have a member who can only initiate proposals (a bot, an integration), another who can only vote (the multisig's human signers), and a designated executor (often any approved member, or a specific operations account that pays for the execution gas).

The flow

  1. Create the multisig. Call multisig_create with members, threshold, time lock. Pay rent for the Multisig account.
  2. Propose a transaction. A member with INITIATE calls vault_transaction_create with the SVM instructions they want the multisig to execute. Then proposal_create to open voting.
  3. Vote. Members with VOTE call proposal_approve or proposal_reject. When approvals reach threshold, status flips to Approved.
  4. Wait for time lock. If time_lock is set, the transaction can't execute until that many seconds have elapsed since approval.
  5. Execute. A member with EXECUTE calls vault_transaction_execute. The multisig program CPIs into the proposed instructions with the vault PDA as signer.

Signing for the vault via CPI

The vault PDA is what holds the actual funds and what signs the proposed instructions. Derivation:

rust
use solana_program::pubkey::Pubkey;

let multisig_pubkey: Pubkey = /* the Multisig account */;
let vault_index: u8 = 0;

let (vault_pda, _bump) = Pubkey::find_program_address(
    &[
        b"multisig",
        multisig_pubkey.as_ref(),
        b"vault",
        &[vault_index],
    ],
    &SQUADS_V4_PROGRAM_ID,
);

When the multisig executes an approved transaction, itinvoke_signed with these vault seeds. Any program that checks is_signer on the vault PDA sees the multisig as the authorised signer.

This is the entire mechanism by which DAOs hold treasuries, protocols control upgrade authorities, and teams custody shared funds on Solana.

TypeScript SDK

typescript
import * as multisig from "@sqds/multisig"
import { Connection, Keypair, PublicKey, TransactionMessage } from "@solana/web3.js"

const conn = new Connection("https://api.mainnet-beta.solana.com")
const creator = Keypair.generate()
const createKey = Keypair.generate()

// 1. Derive the multisig PDA
const [multisigPda] = multisig.getMultisigPda({ createKey: createKey.publicKey })

// 2. Create the multisig (2-of-3 with 1-hour timelock)
await multisig.rpc.multisigCreateV2({
  connection: conn,
  treasury: /* program treasury for fees */,
  creator,
  createKey,
  multisigPda,
  configAuthority: null,            // null = self-managed
  timeLock: 60 * 60,                 // 1 hour
  members: [
    { key: alice.publicKey, permissions: multisig.types.Permissions.all() },
    { key: bob.publicKey,   permissions: multisig.types.Permissions.all() },
    { key: carol.publicKey, permissions: multisig.types.Permissions.all() },
  ],
  threshold: 2,
  rentCollector: null,
})

// 3. Get the vault PDA (index 0 = default treasury)
const [vaultPda] = multisig.getVaultPda({ multisigPda, index: 0 })

// 4. Propose a transaction (e.g. transfer SOL out of the vault)
const transferIx = SystemProgram.transfer({
  fromPubkey: vaultPda,
  toPubkey: recipient,
  lamports: 1_000_000_000,
})

const txMessage = new TransactionMessage({
  payerKey: vaultPda,
  recentBlockhash: (await conn.getLatestBlockhash()).blockhash,
  instructions: [transferIx],
}).compileToV0Message()

await multisig.rpc.vaultTransactionCreate({
  connection: conn,
  feePayer: alice,
  multisigPda,
  transactionIndex: 1n,
  creator: alice.publicKey,
  vaultIndex: 0,
  ephemeralSigners: 0,
  transactionMessage: txMessage,
})

// 5. Create the proposal, vote, execute — same pattern with the SDK
//    multisig.rpc.proposalCreate / proposalApprove / vaultTransactionExecute

Why this pattern wins on Solana

  • Composable signer. The vault PDA is just another signer — any program that accepts a signer accepts the multisig. Stake accounts, upgrade authorities, token mint authorities, oracle authorities — all switchable to a multisig with one SetAuthority call.
  • On-chain transparency. Every proposal, every vote, every execution is a public on-chain transaction. Squads' web UI (squads.so) is just an indexer + a builder — anyone can verify the underlying state.
  • No off-chain coordination layer. Unlike Ethereum Safes, Squads V4 doesn't rely on off-chain signature aggregation. Every vote is its own transaction, posted on-chain.

References

Squads is the boring infrastructure underneath Solana's critical authorities. If you're holding a treasury, an upgrade authority, or any shared signer — this is the program.

Squads: how Solana's standard multisig actually works on-chain | devrels.xyz