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.
Recommended Fix
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.