squads_multisig_program/state/
multisig.rs

1use std::cmp::max;
2
3use anchor_lang::prelude::*;
4use anchor_lang::system_program;
5
6use crate::errors::*;
7use crate::id;
8
9pub const MAX_TIME_LOCK: u32 = 3 * 30 * 24 * 60 * 60; // 3 months
10
11#[account]
12pub struct Multisig {
13    /// Key that is used to seed the multisig PDA.
14    pub create_key: Pubkey,
15    /// The authority that can change the multisig config.
16    /// This is a very important parameter as this authority can change the members and threshold.
17    ///
18    /// The convention is to set this to `Pubkey::default()`.
19    /// In this case, the multisig becomes autonomous, so every config change goes through
20    /// the normal process of voting by the members.
21    ///
22    /// However, if this parameter is set to any other key, all the config changes for this multisig
23    /// will need to be signed by the `config_authority`. We call such a multisig a "controlled multisig".
24    pub config_authority: Pubkey,
25    /// Threshold for signatures.
26    pub threshold: u16,
27    /// How many seconds must pass between transaction voting settlement and execution.
28    pub time_lock: u32,
29    /// Last transaction index. 0 means no transactions have been created.
30    pub transaction_index: u64,
31    /// Last stale transaction index. All transactions up until this index are stale.
32    /// This index is updated when multisig config (members/threshold/time_lock) changes.
33    pub stale_transaction_index: u64,
34    /// The address where the rent for the accounts related to executed, rejected, or cancelled
35    /// transactions can be reclaimed. If set to `None`, the rent reclamation feature is turned off.
36    pub rent_collector: Option<Pubkey>,
37    /// Bump for the multisig PDA seed.
38    pub bump: u8,
39    /// Members of the multisig.
40    pub members: Vec<Member>,
41}
42
43impl Multisig {
44    pub fn size(members_length: usize) -> usize {
45        8  + // anchor account discriminator
46        32 + // create_key
47        32 + // config_authority
48        2  + // threshold
49        4  + // time_lock
50        8  + // transaction_index
51        8  + // stale_transaction_index
52        1  + // rent_collector Option discriminator
53        32 + // rent_collector (always 32 bytes, even if None, just to keep the realloc logic simpler)
54        1  + // bump
55        4  + // members vector length
56        members_length * Member::INIT_SPACE // members
57    }
58
59    pub fn num_voters(members: &[Member]) -> usize {
60        members
61            .iter()
62            .filter(|m| m.permissions.has(Permission::Vote))
63            .count()
64    }
65
66    pub fn num_proposers(members: &[Member]) -> usize {
67        members
68            .iter()
69            .filter(|m| m.permissions.has(Permission::Initiate))
70            .count()
71    }
72
73    pub fn num_executors(members: &[Member]) -> usize {
74        members
75            .iter()
76            .filter(|m| m.permissions.has(Permission::Execute))
77            .count()
78    }
79
80    /// Check if the multisig account space needs to be reallocated to accommodate `members_length`.
81    /// Returns `true` if the account was reallocated.
82    pub fn realloc_if_needed<'a>(
83        multisig: AccountInfo<'a>,
84        members_length: usize,
85        rent_payer: Option<AccountInfo<'a>>,
86        system_program: Option<AccountInfo<'a>>,
87    ) -> Result<bool> {
88        // Sanity checks
89        require_keys_eq!(*multisig.owner, id(), MultisigError::IllegalAccountOwner);
90
91        let current_account_size = multisig.data.borrow().len();
92        let account_size_to_fit_members = Multisig::size(members_length);
93
94        // Check if we need to reallocate space.
95        if current_account_size >= account_size_to_fit_members {
96            return Ok(false);
97        }
98
99        let new_size = max(
100            current_account_size + (10 * Member::INIT_SPACE), // We need to allocate more space. To avoid doing this operation too often, we increment it by 10 members.
101            account_size_to_fit_members,
102        );
103        // Reallocate more space.
104        AccountInfo::realloc(&multisig, new_size, false)?;
105
106        // If more lamports are needed, transfer them to the account.
107        let rent_exempt_lamports = Rent::get().unwrap().minimum_balance(new_size).max(1);
108        let top_up_lamports =
109            rent_exempt_lamports.saturating_sub(multisig.to_account_info().lamports());
110
111        if top_up_lamports > 0 {
112            let system_program = system_program.ok_or(MultisigError::MissingAccount)?;
113            require_keys_eq!(
114                *system_program.key,
115                system_program::ID,
116                MultisigError::InvalidAccount
117            );
118
119            let rent_payer = rent_payer.ok_or(MultisigError::MissingAccount)?;
120
121            system_program::transfer(
122                CpiContext::new(
123                    system_program,
124                    system_program::Transfer {
125                        from: rent_payer,
126                        to: multisig,
127                    },
128                ),
129                top_up_lamports,
130            )?;
131        }
132
133        Ok(true)
134    }
135
136    // Makes sure the multisig state is valid.
137    // This must be called at the end of every instruction that modifies a Multisig account.
138    pub fn invariant(&self) -> Result<()> {
139        let Self {
140            threshold,
141            members,
142            transaction_index,
143            stale_transaction_index,
144            ..
145        } = self;
146        // Max number of members is u16::MAX.
147        require!(
148            members.len() <= usize::from(u16::MAX),
149            MultisigError::TooManyMembers
150        );
151
152        // There must be no duplicate members.
153        let has_duplicates = members.windows(2).any(|win| win[0].key == win[1].key);
154        require!(!has_duplicates, MultisigError::DuplicateMember);
155
156        // Members must not have unknown permissions.
157        require!(
158            members.iter().all(|m| m.permissions.mask < 8), // 8 = Initiate | Vote | Execute
159            MultisigError::UnknownPermission
160        );
161
162        // There must be at least one member with Initiate permission.
163        let num_proposers = Self::num_proposers(members);
164        require!(num_proposers > 0, MultisigError::NoProposers);
165
166        // There must be at least one member with Execute permission.
167        let num_executors = Self::num_executors(members);
168        require!(num_executors > 0, MultisigError::NoExecutors);
169
170        // There must be at least one member with Vote permission.
171        let num_voters = Self::num_voters(members);
172        require!(num_voters > 0, MultisigError::NoVoters);
173
174        // Threshold must be greater than 0.
175        require!(*threshold > 0, MultisigError::InvalidThreshold);
176
177        // Threshold must not exceed the number of voters.
178        require!(
179            usize::from(*threshold) <= num_voters,
180            MultisigError::InvalidThreshold
181        );
182
183        // `state.stale_transaction_index` must be less than or equal to `state.transaction_index`.
184        require!(
185            stale_transaction_index <= transaction_index,
186            MultisigError::InvalidStaleTransactionIndex
187        );
188
189        // Time Lock must not exceed the maximum allowed to prevent bricking the multisig.
190        require!(
191            self.time_lock <= MAX_TIME_LOCK,
192            MultisigError::TimeLockExceedsMaxAllowed
193        );
194
195        Ok(())
196    }
197
198    /// Makes the transactions created up until this moment stale.
199    /// Should be called whenever any multisig parameter related to the voting consensus is changed.
200    pub fn invalidate_prior_transactions(&mut self) {
201        self.stale_transaction_index = self.transaction_index;
202    }
203
204    /// Returns `Some(index)` if `member_pubkey` is a member, with `index` into the `members` vec.
205    /// `None` otherwise.
206    pub fn is_member(&self, member_pubkey: Pubkey) -> Option<usize> {
207        self.members
208            .binary_search_by_key(&member_pubkey, |m| m.key)
209            .ok()
210    }
211
212    pub fn member_has_permission(&self, member_pubkey: Pubkey, permission: Permission) -> bool {
213        match self.is_member(member_pubkey) {
214            Some(index) => self.members[index].permissions.has(permission),
215            _ => false,
216        }
217    }
218
219    /// How many "reject" votes are enough to make the transaction "Rejected".
220    /// The cutoff must be such that it is impossible for the remaining voters to reach the approval threshold.
221    /// For example: total voters = 7, threshold = 3, cutoff = 5.
222    pub fn cutoff(&self) -> usize {
223        Self::num_voters(&self.members)
224            .checked_sub(usize::from(self.threshold))
225            .unwrap()
226            .checked_add(1)
227            .unwrap()
228    }
229
230    /// Add `new_member` to the multisig `members` vec and sort the vec.
231    pub fn add_member(&mut self, new_member: Member) {
232        self.members.push(new_member);
233        self.members.sort_by_key(|m| m.key);
234    }
235
236    /// Remove `member_pubkey` from the multisig `members` vec.
237    ///
238    /// # Errors
239    /// - `MultisigError::NotAMember` if `member_pubkey` is not a member.
240    pub fn remove_member(&mut self, member_pubkey: Pubkey) -> Result<()> {
241        let old_member_index = match self.is_member(member_pubkey) {
242            Some(old_member_index) => old_member_index,
243            None => return err!(MultisigError::NotAMember),
244        };
245
246        self.members.remove(old_member_index);
247
248        Ok(())
249    }
250}
251
252#[derive(AnchorDeserialize, AnchorSerialize, InitSpace, Eq, PartialEq, Clone)]
253pub struct Member {
254    pub key: Pubkey,
255    pub permissions: Permissions,
256}
257
258#[derive(Clone, Copy)]
259pub enum Permission {
260    Initiate = 1 << 0,
261    Vote = 1 << 1,
262    Execute = 1 << 2,
263}
264
265/// Bitmask for permissions.
266#[derive(
267    AnchorSerialize, AnchorDeserialize, InitSpace, Eq, PartialEq, Clone, Copy, Default, Debug,
268)]
269pub struct Permissions {
270    pub mask: u8,
271}
272
273impl Permissions {
274    /// Currently unused.
275    pub fn from_vec(permissions: &[Permission]) -> Self {
276        let mut mask = 0;
277        for permission in permissions {
278            mask |= *permission as u8;
279        }
280        Self { mask }
281    }
282
283    pub fn has(&self, permission: Permission) -> bool {
284        self.mask & (permission as u8) != 0
285    }
286}