Back to Overview
Designing for Squads: A Lesson in Solana Authorization

Designing for Squads: A Lesson in Solana Authorization

February 13, 2026

A design issue we uncovered during our review of WBTC on Solana prevented merchants and custodians from using Squads multisigs for critical operations. We explore how the instruction_sysvar check created an unexpected incompatibility and how a signer-based approach resolved it.

Roles and Operations in WBTC on Solana


The WBTC system on Solana follows a multi-party model where distinct roles (merchants and custodians) interact with a factory program to perform token operations. Merchants can burn WBTC to initiate a redemption, while the custodian can mint new WBTC in response to custody deposits. Both operations are gated by a controller, which enforces that only the factory program can invoke mint and burn.

In practice, the entities behind these roles often operate through multisig wallets rather than simple keypairs. On Solana, Squads is a widely used multisig protocol that allows multiple signers to approve and execute transactions collectively.

How Squads Executes Transactions


Squads follows a batch execution model. A proposer call batch_create() to initialize a new batch, then adds individual instructions via batch_add_transaction(). Once the required threshold of approvals is met, batch_execute_transaction() is called, which executes each added instruction sequentially as a Cross-Program Invocation (CPI).

Critically, when instructions are executed through this mechanism, the source of the CPI call is always the Squads program itself - not the user's wallet, and not the program the user originally intended to invoke.

Note: The function names above refer to the Squads V3 (squads-mpl) interface, which was current at the time of the audit. Squads V4 has since redesigned its interface around vault transactions and batches with different instruction names, but the fundamental execution model is the same: user instructions are executed as CPIs originating from the Squads program.

The Problem: instruction_sysvar Rejects Squads


The original WBTC controller used Solana's instruction_sysvar to verify the origin of incoming calls. Specifically, during mint and burn, the controller would inspect the currently executing instruction via get_instruction_relative() and assert that the calling program matched the factory's program ID:

<pre><code>
let caller_program_id =
   anchor_lang::solana_program::sysvar::instructions::get_instruction_relative(
       0,
       &ctx.accounts.instruction_sysvar.to_account_info(),
   )?
   .program_id;

require!(
   caller_program_id == ctx.accounts.controller_store.factory,
   CustomError::Unauthorized
);
</code></pre>

This check works as expected when a user invokes the factory directly: the top-level instruction is the factory call, and the instruction_sysvar correctly reflects this.

However, when the same operation is routed through Squads, the currently executing top-level instruction is batch_execute_transaction() - a Squads program instruction. The factory call now happens as a CPI within the Squads execution context, meaning get_instruction_relative(0, ...) returns the Squads program ID instead of the factory's.

The result: merchants using Squads cannot burn WBTC, and a custodian using Squads cannot mint WBTC, even though they are fully authorized to do so. The access control mechanism, while conceptually sound, was incompatible with the CPI execution model of multisig programs.

The Fix: Signer-Based Authorization


The solution replaced the instruction_sysvar check with a signer-based approach. Instead of introspecting the instruction stack to determine who initiated the call, the factory now signs the CPI to the controller using the factory_store PDA as a signer.

On the controller side, the mint and burn instructions simply require that the factory_store account is a signer of the transaction. Since only the factory program can produce a valid PDA signature for factory_store, this guarantees that the call originated from the factory — regardless of how many layers of CPI sit above it.

This approach decouples the authorization check from the instruction execution context entirely. Whether a merchant calls the factory directly or through Squads (or any future multisig or governance framework), the factory will always sign with its PDA, and the controller will always be able to verify it.

Takeaways

  • Solana's instruction_sysvar provides access to all top-level instructions in the transaction, but does not include instructions executed via CPI. Programs that rely on it to verify their caller will only see the outermost instruction in the call chain, not the intermediate program that actually invoked them.
  • CPI-based authorization via PDA signers is more robust. A PDA signature is unforgeable and context-independent. It doesn't matter how deeply nested the call is or which program sits at the top of the instruction stack.
  • Multisig usage should be a first-class consideration when designing role-based systems. If critical roles like merchants or custodians are expected to operate through multisigs, the program's authorization model must accommodate this from the start.