Skip to main content
Sigvex

Multi-Sig Validation

Detects multi-signature validation issues including weak thresholds, missing signer verification, and replay vulnerabilities.

Multi-Sig Validation

Overview

Remediation Guide: How to Fix Multi-Sig Validation Issues

The multi-sig validation detector identifies weaknesses in multi-signature implementations on Solana. It flags programs with a threshold of 1-of-N (which defeats the purpose of multi-sig), missing signer verification in multi-sig functions, absent replay protection, and missing signer count validation. These issues can allow a single compromised key to execute operations that should require multiple approvals.

Why This Is an Issue

Multi-signature schemes protect high-value operations by requiring multiple independent parties to approve. Flawed implementations undermine this protection:

  • Weak thresholds (1-of-N, CWE-330): A single compromised key grants full access, providing no benefit over a single-key scheme.
  • Missing signer verification (CWE-284): If signers are not individually checked, the program trusts that the correct accounts are present without verifying their signatures.
  • Missing replay protection (CWE-294): Without nonces or unique transaction identifiers, a previously approved multi-sig transaction can be replayed to execute the same operation multiple times.
  • Missing signer count validation: Even if individual signers are checked, failure to verify the total count meets the threshold allows execution with fewer signers than required.

How to Resolve

Native Solana

use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey};

pub fn execute_multisig(
    accounts: &[AccountInfo],
    threshold: u8,
    nonce: u64,
) -> Result<(), ProgramError> {
    let multisig_state = &accounts[0];
    let signers = &accounts[1..];

    // 1. Verify threshold is reasonable
    if threshold < 2 {
        return Err(ProgramError::InvalidArgument);
    }

    // 2. Verify each signer
    let mut valid_signer_count: u8 = 0;
    let state_data = multisig_state.try_borrow_data()?;
    let stored_signers = parse_signers(&state_data);

    for signer in signers {
        if !signer.is_signer {
            continue;
        }
        if stored_signers.contains(signer.key) {
            valid_signer_count += 1;
        }
    }

    // 3. Verify signer count meets threshold
    if valid_signer_count < threshold {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // 4. Replay protection: verify and increment nonce
    let stored_nonce = u64::from_le_bytes(state_data[64..72].try_into().unwrap());
    if nonce != stored_nonce {
        return Err(ProgramError::InvalidArgument);
    }

    // Execute and update nonce
    drop(state_data);
    let mut data = multisig_state.try_borrow_mut_data()?;
    data[64..72].copy_from_slice(&(nonce + 1).to_le_bytes());
    Ok(())
}

Anchor

#[derive(Accounts)]
pub struct ExecuteMultisig<'info> {
    #[account(
        mut,
        constraint = multisig.threshold >= 2 @ ErrorCode::WeakThreshold,
        constraint = multisig.nonce == expected_nonce @ ErrorCode::ReplayAttempt
    )]
    pub multisig: Account<'info, MultisigState>,
    // Remaining accounts are signers validated in handler
}

Examples

Vulnerable Code

pub fn multisig_transfer(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let vault = &accounts[0];
    let dest = &accounts[1];

    // VULNERABLE: no signer checks, no threshold, no replay protection
    **vault.try_borrow_mut_lamports()? -= amount;
    **dest.try_borrow_mut_lamports()? += amount;
    Ok(())
}

Fixed Code

pub fn multisig_transfer(
    accounts: &[AccountInfo],
    amount: u64,
    nonce: u64,
) -> ProgramResult {
    let multisig_state = &accounts[0];
    let vault = &accounts[1];
    let dest = &accounts[2];
    let signers = &accounts[3..];

    // Validate multi-sig
    let state = MultisigState::deserialize(&multisig_state.try_borrow_data()?)?;

    if nonce != state.nonce { return Err(ProgramError::InvalidArgument); }

    let mut count = 0u8;
    for signer in signers {
        if signer.is_signer && state.signers.contains(signer.key) {
            count += 1;
        }
    }
    if count < state.threshold {
        return Err(ProgramError::MissingRequiredSignature);
    }

    **vault.try_borrow_mut_lamports()? -= amount;
    **dest.try_borrow_mut_lamports()? += amount;

    // Update nonce for replay protection
    let mut data = multisig_state.try_borrow_mut_data()?;
    MultisigState { nonce: nonce + 1, ..state }.serialize(&mut &mut data[..])?;
    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "multisig-validation",
  "severity": "high",
  "confidence": 0.78,
  "title": "Weak Multi-Sig Threshold (1-of-N)",
  "description": "Multi-signature threshold is set to 1, requiring only a single signer. This defeats the purpose of multi-signature protection. A single compromised key can execute critical operations.",
  "location": { "function": "multisig_execute", "block": 0, "statement": 0 },
  "cwe": 330
}

Detection Methodology

The detector analyzes multi-signature patterns in the function’s intermediate representation:

  1. Signer check tracking: Records all CheckSigner statements to determine how many individual signers are verified.
  2. Threshold extraction: Examines constant assignments and comparisons for small values (< 10) that represent multi-sig thresholds. A threshold of 1 generates a weak-threshold finding.
  3. Multi-sig function identification: Functions with “multisig” in their name are expected to have signer verification and replay protection.
  4. Signer count validation: When multiple signers are checked but no threshold comparison exists, a missing-count-validation finding is generated.
  5. Replay protection scanning: Checks for nonce or signature-based replay prevention patterns in multi-sig functions.
  6. Context adjustment: Confidence is reduced for Anchor programs and read-only functions.

Limitations

False positives:

  • Programs using external multi-sig services (e.g., Squads Protocol) where validation occurs off-chain or in a separate program.
  • Functions named “multisig” that are informational rather than transactional.

False negatives:

  • Custom multi-sig implementations that use non-standard patterns the heuristics do not recognize.
  • Multi-sig validation delegated to CPI calls to governance programs.

References