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:
- Signer check tracking: Records all
CheckSignerstatements to determine how many individual signers are verified. - 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.
- Multi-sig function identification: Functions with “multisig” in their name are expected to have signer verification and replay protection.
- Signer count validation: When multiple signers are checked but no threshold comparison exists, a missing-count-validation finding is generated.
- Replay protection scanning: Checks for nonce or signature-based replay prevention patterns in multi-sig functions.
- 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.
Related Detectors
- Admin Key Management — single admin key without multi-sig
- Signer Authority Role — signer without authority role validation
- Instruction Sender Validation — missing sender authority validation