solana-squads-multisig — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited solana-squads-multisig (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Squads is the dominant Solana multisig — the Solana ecosystem's equivalent of Gnosis Safe. The V4 protocol (program ID SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf on mainnet-beta and devnet) is what real teams use today to secure program upgrade authority, custody team treasuries, hold large LP positions, and run on-chain DAO operations. The TypeScript SDK (@sqds/multisig) exposes three flavors of every action — multisig.instructions.* (returns an Ix you compose yourself), multisig.transactions.* (returns a signed VersionedTransaction), and multisig.rpc.* (builds, sends, and returns a signature). Most automation should target multisig.rpc.* on the backend and multisig.instructions.* from a wallet adapter frontend.
Trigger this skill when the user wants to:
upgrade_authority off a single keypair onto a multisig (the #1 production use case — single-key upgrade authority is the most common source of catastrophic Solana program compromises)If the user wants a single-sig hot wallet, an SPL token mint, or a generic Solana transaction, this is the wrong skill.
Pin these. The Squads program has been stable, but the SDK and @solana/web3.js v1 vs v2 split means version mismatches cause cryptic runtime errors.
{
"engines": { "node": ">=20" },
"dependencies": {
"@sqds/multisig": "2.1.4",
"@solana/web3.js": "^1.95.0"
},
"devDependencies": {
"typescript": "^5.4.0",
"ts-node": "^10.9.0",
"@types/node": "^20.0.0"
}
}Notes:
@sqds/multisig 2.1.4 (latest as of May 2026) targets @solana/web3.js v1. Do NOT use @solana/web3.js v2 (kit) with this SDK — wrong types, wrong Connection, wrong Keypair.solana airdrop 2 is usually enough for a full create + propose + execute cycle.https://api.devnet.solana.com) is fine for testing; for mainnet use Helius / Triton / QuickNode.A Squads multisig has four distinct PDAs you will derive:
| PDA | What it is | When you need it |
|---|---|---|
Multisig account (multisigPda) | The on-chain account that stores members, threshold, transaction index. Derived from a one-time random createKey. | Every call. This is "the multisig." |
Vault PDA (vaultPda, by index) | The actual signer that holds funds and signs CPIs. One multisig has many vaults (index 0, 1, 2, ...). Index 0 is the default. | Funding, sending from the treasury, setting as program upgrade authority. |
| Transaction PDA | Holds a proposed transaction's message. | Internal — derived per transactionIndex. |
| Proposal PDA | Tracks votes (approve / reject / cancel). One per transaction index. | When approving or executing. |
The vault PDA is what you put funds into and what becomes the program upgrade authority — never the multisig account itself.
multisig.rpc.multisigCreateV2() takes:
createKey — a one-time Keypair whose pubkey seeds the multisigPda. Discard after; not used to sign anything later.creator — the Keypair paying for the create transaction.members[] — array of { key: PublicKey, permissions: Permissions }. Permissions are bitfields built from Permission.Initiate | Permission.Vote | Permission.Execute. Use Permissions.all() for full power.threshold — minimum approvals required (e.g. 2 for 2-of-3).configAuthority — null for an autonomous multisig (governs itself via configTransaction); set to a pubkey for a controlled multisig where that authority can unilaterally change membership. Most teams want null.timeLock — seconds between approval-reaching-threshold and execution. 0 for none. Use a real timelock (1–24h) for program upgrade authority multisigs.rentCollector — null is fine; set to recover rent from closed transaction accounts.treasury — the treasury field from ProgramConfig account; the create-fee goes here.const [vaultPda] = multisig.getVaultPda({ multisigPda, index: 0 });Index 0 is the default vault every multisig has. You can derive vault 1, 2, ... and use them as separate sub-treasuries under the same governance.
A vault is a normal PDA from the multisig's perspective — send SOL or SPL tokens to vaultPda like any other address. It will not show up in getMultisigInfo; check it with connection.getBalance(vaultPda).
A "vault transaction" is a proposed CPI bundle the vault will sign. You build a TransactionMessage whose payerKey is the vault PDA (not your wallet), with the instructions you want the vault to execute.
const transferMessage = new TransactionMessage({
payerKey: vaultPda,
recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
instructions: [SystemProgram.transfer({ fromPubkey: vaultPda, toPubkey: dest, lamports })],
});
const newIndex = BigInt(Number(multisigInfo.transactionIndex) + 1);
await multisig.rpc.vaultTransactionCreate({
connection, feePayer: proposer, multisigPda,
transactionIndex: newIndex,
creator: proposer.publicKey,
vaultIndex: 0,
ephemeralSigners: 0,
transactionMessage: transferMessage,
memo: "Transfer 0.01 SOL",
});vaultTransactionCreate only stores the message — members can't vote yet. You must call proposalCreate at the same transactionIndex:
await multisig.rpc.proposalCreate({ connection, feePayer, multisigPda, transactionIndex: newIndex, creator });Pass isDraft: true if you're building a batch and want to add more before activating.
Each member with Permission.Vote calls proposalApprove with their own Keypair. The proposal moves to Approved state when threshold is reached:
await multisig.rpc.proposalApprove({ connection, feePayer, multisigPda, transactionIndex: newIndex, member: memberKeypair });Any member with Permission.Execute can fire it once Approved:
await multisig.rpc.vaultTransactionExecute({
connection, feePayer: executor, multisigPda,
transactionIndex: newIndex,
member: executor.publicKey,
signers: [executor],
sendOptions: { skipPreflight: true },
});skipPreflight: true is often needed — Squads' CPIs can confuse the simulator into reporting false errors. Confirm the on-chain signature for ground truth.
Use the webapp (https://v4.squads.so) when:
Use the SDK when:
A multisig created via SDK is fully visible / usable in the webapp and vice versa — they're the same on-chain program.
The single highest-leverage thing this skill enables. After deploying a program:
timeLock ≥ 86400 (24h).solana program set-upgrade-authority <PROGRAM_ID> --new-upgrade-authority <VAULT_PDA>.program deploy must be proposed as a BPFLoaderUpgradeable.upgrade instruction inside a vault transaction, approved, wait out the timelock, then executed.The timelock means a single compromised key can't push a malicious upgrade in the time it takes to alert your team.
Changing members, threshold, or configAuthority is not a vaultTransactionCreate — it's a configTransactionCreate. Different PDA, different instruction, different proposal type. Common mistakes:
addMember instruction inside a vault transaction. Won't work — the multisig account is only writable to itself via configTransaction.configAuthority is set), the config authority can change membership directly without a vote. For autonomous multisigs (the default), member changes require the same threshold vote as any other action.Always rotate one member at a time and verify the on-chain state between rotations.
getVaultPda({ multisigPda, index: 0 }) for anything involving value.proposalCreate, no one can vote and you'll get cryptic "proposal not found" errors at the approve step.vaultIndex passed to vaultTransactionCreate MUST match the index used to derive the payerKey of the TransactionMessage.proposalCreate puts the proposal directly into Active. But proposalCreate({ isDraft: true }) (used for batches) leaves it as Draft — you must call proposalActivate before any approve will succeed. Symptom: InvalidProposalStatus error.configTransactionCreate + configTransactionExecute — a completely separate flow from value-moving vault transactions. The instructions live under multisig.rpc.configTransaction*. See docs.squads.so/main/development/typescript/instructions/controlled-multisig-instructions.createKey seeds the multisigPda. If you accidentally reuse the same createKey.publicKey, the second multisigCreateV2 will fail with "account already exists." Always generate a fresh Keypair per multisig.@sqds/multisig 2.1.4 is built against web3.js v1. If your project upgraded to web3.js v2 / kit, you'll get TypeScript errors about Keypair and Connection types not matching. Either pin web3.js v1 for the multisig surface or wait for an SDK v3.treasury (required) is the program's fee recipient — set it from ProgramConfig.treasury, not your own wallet. rentCollector (optional, can be null) is where rent flows when transaction accounts close — set it to your multisig vault if you want to recover that rent.transactionIndex. Always read it from Multisig.fromAccountAddress(...).transactionIndex, add 1, pass as BigInt. Hardcoding 1n only works for the very first transaction.SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCfSee scripts/create-multisig.ts and scripts/propose-and-execute.ts. Both run against devnet out of the box.
# Setup
npm init -y
npm i @sqds/[email protected] @solana/web3.js@^1.95.0
npm i -D typescript ts-node @types/node
# 1. Create the multisig and fund vault 0
npx ts-node scripts/create-multisig.ts
# Copy the printed `MULTISIG_PDA` and paste it into propose-and-execute.ts
# (or set it via env var — see the script).
# 2. Run the full propose -> approve x2 -> execute flow
npx ts-node scripts/propose-and-execute.ts~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.