Skip to main content

hopper_multisig/
multisig.rs

1//! Multi-signer threshold verification.
2//!
3//! M-of-N signature checking for governance, multisig wallets, and admin
4//! operations. Counts signers, checks thresholds, and prevents the
5//! duplicate-signer attack (same account passed in multiple slots).
6
7use hopper_runtime::{error::ProgramError, AccountView, ProgramResult};
8
9/// Count how many accounts in the slice are transaction signers.
10#[inline(always)]
11pub fn count_signers(accounts: &[&AccountView]) -> u8 {
12    let mut n: u8 = 0;
13    let mut i = 0;
14    while i < accounts.len() {
15        if accounts[i].is_signer() {
16            n = n.saturating_add(1);
17        }
18        i += 1;
19    }
20    n
21}
22
23/// Require at least `threshold` of the provided accounts to be signers.
24///
25/// Also checks that all accounts have unique addresses to prevent the
26/// duplicate-signer attack (passing the same signer key in multiple slots).
27///
28/// Returns `MissingRequiredSignature` if fewer than `threshold` are signers.
29/// Returns `InvalidArgument` if duplicate addresses are found.
30#[inline(always)]
31pub fn check_threshold(accounts: &[&AccountView], threshold: u8) -> ProgramResult {
32    // Check uniqueness (O(n^2) but n is always small, typically 3-9)
33    let len = accounts.len();
34    let mut i = 0;
35    while i < len {
36        let mut j = i + 1;
37        while j < len {
38            if accounts[i].address() == accounts[j].address() {
39                return Err(ProgramError::InvalidArgument);
40            }
41            j += 1;
42        }
43        i += 1;
44    }
45
46    let signers = count_signers(accounts);
47    if signers < threshold {
48        return Err(ProgramError::MissingRequiredSignature);
49    }
50    Ok(())
51}
52
53/// Require ALL provided accounts to be signers (N-of-N).
54///
55/// Also checks uniqueness. Use this for operations that require
56/// unanimous consent.
57#[inline(always)]
58pub fn check_all_signers(accounts: &[&AccountView]) -> ProgramResult {
59    let len = accounts.len();
60    if len > u8::MAX as usize {
61        return Err(ProgramError::InvalidArgument);
62    }
63    check_threshold(accounts, len as u8)
64}
65
66/// Require exactly one of the provided accounts to be a signer (1-of-N).
67///
68/// Useful for "any admin can act" patterns. Checks uniqueness.
69#[inline(always)]
70pub fn check_any_signer(accounts: &[&AccountView]) -> ProgramResult {
71    check_threshold(accounts, 1)
72}