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}