Skip to main content
Sigvex

Multi-Sig Validation Remediation

How to fix multi-signature validation weaknesses including thresholds, replay protection, and signer verification.

Multi-Sig Validation Remediation

Overview

Related Detector: Multi-Sig Validation

Multi-sig validation issues arise when threshold requirements are too low, signers are not individually verified, or replay protection is missing. The fix involves setting thresholds to at least 2, verifying each signer against a stored list, validating the total count, and using nonces to prevent replay.

Before (Vulnerable)

pub fn multisig_execute(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let vault = &accounts[0];
    let dest = &accounts[1];
    // No signer verification, no threshold, no replay protection
    **vault.try_borrow_mut_lamports()? -= amount;
    **dest.try_borrow_mut_lamports()? += amount;
    Ok(())
}

After (Fixed)

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

    let state = MultisigState::deserialize(&multisig_account.try_borrow_data()?)?;

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

    // Verify signers against stored list
    let mut valid = 0u8;
    for signer in signers {
        if signer.is_signer && state.signers.contains(signer.key) {
            valid += 1;
        }
    }

    // Threshold check (threshold must be >= 2)
    if valid < state.threshold || state.threshold < 2 {
        return Err(ProgramError::MissingRequiredSignature);
    }

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

    // Increment nonce
    let mut data = multisig_account.try_borrow_mut_data()?;
    MultisigState { nonce: nonce + 1, ..state }.serialize(&mut &mut data[..])?;
    Ok(())
}

Alternative Mitigations

1. Use Squads Protocol

Integrate with Squads (formerly Squads Multisig) for battle-tested multi-sig execution:

// Use Squads vault as the authority
#[derive(Accounts)]
pub struct SquadsAction<'info> {
    #[account(constraint = authority.key() == config.squads_vault @ ErrorCode::Unauthorized)]
    pub authority: Signer<'info>,
    pub config: Account<'info, Config>,
}

2. Time-delayed execution

Combine multi-sig with a timelock for defense in depth:

pub fn propose(ctx: Context<Propose>, action: Action) -> Result<()> {
    ctx.accounts.proposal.action = action;
    ctx.accounts.proposal.execute_after = Clock::get()?.unix_timestamp + DELAY;
    ctx.accounts.proposal.approvals = 0;
    Ok(())
}

pub fn execute(ctx: Context<Execute>) -> Result<()> {
    require!(ctx.accounts.proposal.approvals >= THRESHOLD, NotEnoughApprovals);
    require!(Clock::get()?.unix_timestamp >= ctx.accounts.proposal.execute_after, TooEarly);
    // Execute
    Ok(())
}

Common Mistakes

Mistake 1: Setting threshold to 1

A 1-of-N threshold provides no additional security over a single key. Always require threshold >= 2.

Mistake 2: Not validating signers against a stored list

// WRONG: counts all signers, not just authorized ones
let count = accounts.iter().filter(|a| a.is_signer).count();

Verify each signer’s key is in the authorized signer list.

Mistake 3: Forgetting replay protection

Without a nonce, an approved transaction can be submitted multiple times. Always increment a nonce or mark the proposal as executed after successful execution.

References