squads_mpl/
lib.rs

1/*
2    Squads Multisig Program - Program & Instructions
3    https://github.com/squads-protocol/squads-mpl
4*/
5
6use anchor_lang::{
7    prelude::*,
8    solana_program::{
9        instruction::Instruction,
10        program::invoke_signed
11    }
12};
13
14use hex::FromHex;
15
16use state::*;
17use errors::*;
18use account::*;
19
20pub mod state;
21pub mod account;
22pub mod errors;
23
24#[cfg(not(feature = "no-entrypoint"))]
25use {default_env::default_env, solana_security_txt::security_txt};
26
27#[cfg(not(feature = "no-entrypoint"))]
28security_txt! {
29    name: "Squads MPL",
30    project_url: "https://squads.so",
31    contacts: "email:security@sqds.io,email:contact@osec.io",
32    policy: "https://github.com/Squads-Protocol/squads-mpl/blob/main/SECURITY.md",
33    preferred_languages: "en",
34    source_code: "https://github.com/squads-protocol/squads-mpl",
35    source_revision: default_env!("GITHUB_SHA", "aa264525559014c58cacf8fe2cdf3fc594511c06"),
36    auditors: "OtterSec, Neodyme"
37}
38
39declare_id!("SMPLecH534NA9acpos4G6x7uf3LWbCAwZQE9e8ZekMu");
40
41#[program]
42pub mod squads_mpl {
43
44    use std::convert::TryInto;
45
46    use super::*;
47
48    /// Creates a new multisig account
49    // instruction to create a multisig
50    pub fn create(
51        ctx: Context<Create>,
52        threshold: u16,       // threshold of members required to sign
53        create_key: Pubkey,   // the public key used to seed the original multisig creation
54        members: Vec<Pubkey>, // a list of members (Public Keys) to use for the multisig
55        _meta: String,        // a string of metadata that can be used to describe the multisig on-chain as a memo ie. '{"name":"My Multisig","description":"This is a my multisig"}'
56    ) -> Result<()> {
57        // sort the members and remove duplicates
58        let mut members = members;
59        members.sort();
60        members.dedup();
61
62        // check we don't exceed u16
63        let total_members = members.len();
64        if total_members < 1 {
65            return err!(MsError::EmptyMembers);
66        }
67
68        // make sure we don't exceed u16 on first call
69        if total_members > usize::from(u16::MAX) {
70            return err!(MsError::MaxMembersReached);
71        }
72
73        // make sure threshold is valid
74        if usize::from(threshold) < 1 || usize::from(threshold) > total_members {
75            return err!(MsError::InvalidThreshold);
76        }
77
78        ctx.accounts.multisig.init(
79            threshold,
80            create_key,
81            members,
82            *ctx.bumps.get("multisig").unwrap(),
83        )
84    }
85
86    /// The instruction to add a new member to the multisig.
87    /// Adds member/key to the multisig and reallocates space if neccessary
88    /// If the multisig needs to be reallocated, it must be prefunded with
89    /// enough lamports to cover the new size.
90    pub fn add_member(ctx: Context<MsAuthRealloc>, new_member: Pubkey) -> Result<()> {
91        // if max is already reached, we can't have more members
92        if ctx.accounts.multisig.keys.len() >= usize::from(u16::MAX) {
93            return err!(MsError::MaxMembersReached);
94        }
95
96        // check if realloc is needed
97        let multisig_account_info = ctx.accounts.multisig.to_account_info();
98        if *multisig_account_info.owner != squads_mpl::ID {
99            return err!(MsError::InvalidInstructionAccount);
100        }
101        let curr_data_size = multisig_account_info.data.borrow().len();
102        let spots_left =
103            ((curr_data_size - Ms::SIZE_WITHOUT_MEMBERS) / 32) - ctx.accounts.multisig.keys.len();
104
105        // if not enough, add (10 * 32) to size - bump it up by 10 accounts
106        if spots_left < 1 {
107            // add space for 10 more keys
108            let needed_len = curr_data_size + (10 * 32);
109            // reallocate more space
110            AccountInfo::realloc(&multisig_account_info, needed_len, false)?;
111            // if more lamports are needed, transfer them to the account
112            let rent_exempt_lamports = ctx.accounts.rent.minimum_balance(needed_len).max(1);
113            let top_up_lamports = rent_exempt_lamports
114                .saturating_sub(ctx.accounts.multisig.to_account_info().lamports());
115            if top_up_lamports > 0 {
116                return err!(MsError::NotEnoughLamports);
117            }
118        }
119        ctx.accounts.multisig.reload()?;
120        ctx.accounts.multisig.add_member(new_member)?;
121        let new_index = ctx.accounts.multisig.transaction_index;
122        // set the change index, which will deprecate any active transactions
123        ctx.accounts.multisig.set_change_index(new_index)
124    }
125
126    /// The instruction to remove a member from the multisig
127    pub fn remove_member(ctx: Context<MsAuth>, old_member: Pubkey) -> Result<()> {
128        // if there is only one key in this multisig, reject the removal
129        if ctx.accounts.multisig.keys.len() == 1 {
130            return err!(MsError::CannotRemoveSoloMember);
131        }
132        ctx.accounts.multisig.remove_member(old_member)?;
133
134        // if the number of keys is now less than the threshold, adjust it
135        if ctx.accounts.multisig.keys.len() < usize::from(ctx.accounts.multisig.threshold) {
136            let new_threshold: u16 = ctx.accounts.multisig.keys.len().try_into().unwrap();
137            ctx.accounts.multisig.change_threshold(new_threshold)?;
138        }
139        let new_index = ctx.accounts.multisig.transaction_index;
140        // update the change index to deprecate any active transactions
141        ctx.accounts.multisig.set_change_index(new_index)
142    }
143
144    /// The instruction to change the threshold of the multisig and simultaneously remove a member
145    pub fn remove_member_and_change_threshold<'info>(
146        ctx: Context<'_, '_, '_, 'info, MsAuth<'info>>,
147        old_member: Pubkey,
148        new_threshold: u16,
149    ) -> Result<()> {
150        remove_member(
151            Context::new(
152                ctx.program_id,
153                ctx.accounts,
154                ctx.remaining_accounts,
155                ctx.bumps.clone(),
156            ),
157            old_member,
158        )?;
159        change_threshold(ctx, new_threshold)
160    }
161
162    /// The instruction to change the threshold of the multisig and simultaneously add a member
163    pub fn add_member_and_change_threshold<'info>(
164        ctx: Context<'_, '_, '_, 'info, MsAuthRealloc<'info>>,
165        new_member: Pubkey,
166        new_threshold: u16,
167    ) -> Result<()> {
168        // add the member
169        add_member(
170            Context::new(
171                ctx.program_id,
172                ctx.accounts,
173                ctx.remaining_accounts,
174                ctx.bumps.clone(),
175            ),
176            new_member,
177        )?;
178
179        // check that the threshold value is valid
180        if ctx.accounts.multisig.keys.len() < usize::from(new_threshold) {
181            let new_threshold: u16 = ctx.accounts.multisig.keys.len().try_into().unwrap();
182            ctx.accounts.multisig.change_threshold(new_threshold)?;
183        } else if new_threshold < 1 {
184            return err!(MsError::InvalidThreshold);
185        } else {
186            ctx.accounts.multisig.change_threshold(new_threshold)?;
187        }
188        let new_index = ctx.accounts.multisig.transaction_index;
189        // update the change index to deprecate any active transactions
190        ctx.accounts.multisig.set_change_index(new_index)
191    }
192
193    /// The instruction to change the threshold of the multisig
194    pub fn change_threshold(ctx: Context<MsAuth>, new_threshold: u16) -> Result<()> {
195        // if the new threshold value is valid
196        if ctx.accounts.multisig.keys.len() < usize::from(new_threshold) {
197            let new_threshold: u16 = ctx.accounts.multisig.keys.len().try_into().unwrap();
198            ctx.accounts.multisig.change_threshold(new_threshold)?;
199        } else if new_threshold < 1 {
200            return err!(MsError::InvalidThreshold);
201        } else {
202            ctx.accounts.multisig.change_threshold(new_threshold)?;
203        }
204        let new_index = ctx.accounts.multisig.transaction_index;
205        // update the change index to deprecate any active transactions
206        ctx.accounts.multisig.set_change_index(new_index)
207    }
208
209    /// instruction to increase the authority value tracked in the multisig
210    /// This is optional, as authorities are simply PDAs, however it may be helpful
211    /// to keep track of commonly used authorities in a UI.
212    /// This has no functional impact on the multisig or its functionality, but
213    /// can be used to track commonly used authorities (ie, vault 1, vault 2, etc.)
214    pub fn add_authority(ctx: Context<MsAuth>) -> Result<()> {
215        ctx.accounts.multisig.add_authority()
216    }
217
218    /// Instruction to create a multisig transaction.
219    /// Each transaction is tied to a single authority, and must be specified when
220    /// creating the instruction below. authority 0 is reserved for internal
221    /// instructions, whereas authorities 1 or greater refer to a vault,
222    /// upgrade authority, or other.
223    pub fn create_transaction(ctx: Context<CreateTransaction>, authority_index: u32) -> Result<()> {
224        let ms = &mut ctx.accounts.multisig;
225        let authority_bump = match authority_index {
226            1.. => {
227                let (_, auth_bump) = Pubkey::find_program_address(
228                    &[
229                        b"squad",
230                        ms.key().as_ref(),
231                        &authority_index.to_le_bytes(),
232                        b"authority",
233                    ],
234                    ctx.program_id,
235                );
236                auth_bump
237            }
238            0 => ms.bump,
239        };
240
241        ms.transaction_index = ms.transaction_index.checked_add(1).unwrap();
242        ctx.accounts.transaction.init(
243            ctx.accounts.creator.key(),
244            ms.key(),
245            ms.transaction_index,
246            *ctx.bumps.get("transaction").unwrap(),
247            authority_index,
248            authority_bump,
249        )
250    }
251
252    /// Instruction to set the state of a transaction "active".
253    /// "active" transactions can then be signed off by multisig members
254    pub fn activate_transaction(ctx: Context<ActivateTransaction>) -> Result<()> {
255        ctx.accounts.transaction.activate()
256    }
257
258    /// Instruction to attach an instruction to a transaction.
259    /// Transactions must be in the "draft" status, and any
260    /// signer (aside from execution payer) specified in an instruction 
261    /// must match the authority PDA specified during the transaction creation.
262    pub fn add_instruction(
263        ctx: Context<AddInstruction>,
264        incoming_instruction: IncomingInstruction,
265    ) -> Result<()> {
266        let tx = &mut ctx.accounts.transaction;
267        // make sure internal transactions have a matching program id for attached instructions
268        if tx.authority_index == 0 && &incoming_instruction.program_id != ctx.program_id {
269            return err!(MsError::InvalidAuthorityIndex);
270        }
271        tx.instruction_index = tx.instruction_index.checked_add(1).unwrap();
272        ctx.accounts.instruction.init(
273            tx.instruction_index,
274            incoming_instruction,
275            *ctx.bumps.get("instruction").unwrap(),
276        )
277    }
278
279    /// Instruction to approve a transaction on behalf of a member.
280    /// The transaction must have an "active" status
281    pub fn approve_transaction(ctx: Context<VoteTransaction>) -> Result<()> {
282        // if they have previously voted to reject, remove that item (change vote check)
283        if let Some(ind) = ctx
284            .accounts
285            .transaction
286            .has_voted_reject(ctx.accounts.member.key())
287        {
288            ctx.accounts.transaction.remove_reject(ind)?;
289        }
290
291        // if they haven't already approved
292        if ctx
293            .accounts
294            .transaction
295            .has_voted_approve(ctx.accounts.member.key())
296            .is_none()
297        {
298            ctx.accounts.transaction.sign(ctx.accounts.member.key())?;
299        }
300
301        // if current number of signers reaches threshold, mark the transaction as execute ready
302        if ctx.accounts.transaction.approved.len() >= usize::from(ctx.accounts.multisig.threshold) {
303            ctx.accounts.transaction.ready_to_execute()?;
304        }
305        Ok(())
306    }
307
308    /// Instruction to reject a transaction.
309    /// The transaction must have an "active" status.
310    pub fn reject_transaction(ctx: Context<VoteTransaction>) -> Result<()> {
311        // if they have previously voted to approve, remove that item (change vote check)
312        if let Some(ind) = ctx
313            .accounts
314            .transaction
315            .has_voted_approve(ctx.accounts.member.key())
316        {
317            ctx.accounts.transaction.remove_approve(ind)?;
318        }
319
320        // check if they haven't already voted reject
321        if ctx
322            .accounts
323            .transaction
324            .has_voted_reject(ctx.accounts.member.key())
325            .is_none()
326        {
327            ctx.accounts.transaction.reject(ctx.accounts.member.key())?;
328        }
329
330        // ie total members 7, threshold 3, cutoff = 4
331        // ie total member 8, threshold 6, cutoff = 2
332        let cutoff = ctx
333            .accounts
334            .multisig
335            .keys
336            .len()
337            .checked_sub(usize::from(ctx.accounts.multisig.threshold))
338            .unwrap();
339        if ctx.accounts.transaction.rejected.len() > cutoff {
340            ctx.accounts.transaction.set_rejected()?;
341        }
342        Ok(())
343    }
344
345    /// Instruction to cancel a transaction.
346    /// Transactions must be in the "executeReady" status.
347    /// Transaction will only be cancelled if the number of
348    /// cancellations reaches the threshold. A cancelled
349    /// transaction will no longer be able to be executed.
350    pub fn cancel_transaction(ctx: Context<CancelTransaction>) -> Result<()> {
351        // check if they haven't cancelled yet
352        if ctx
353            .accounts
354            .transaction
355            .has_cancelled(ctx.accounts.member.key())
356            .is_none()
357        {
358            ctx.accounts.transaction.cancel(ctx.accounts.member.key())?
359        }
360
361        // if the current number of signers reaches threshold, mark the transaction as "cancelled"
362        if ctx.accounts.transaction.cancelled.len() >= usize::from(ctx.accounts.multisig.threshold)
363        {
364            ctx.accounts.transaction.set_cancelled()?;
365        }
366        Ok(())
367    }
368
369    /// Instruction to execute a transaction.
370    /// Transaction status must be "executeReady", and the account list must match
371    /// the unique indexed accounts in the following manner: 
372    /// [ix_1_account, ix_1_program_account, ix_1_remaining_account_1, ix_1_remaining_account_2, ...]
373    /// 
374    /// Refer to the README for more information on how to construct the account list.
375    pub fn execute_transaction<'info>(
376        ctx: Context<'_, '_, '_, 'info, ExecuteTransaction<'info>>,
377        account_list: Vec<u8>,
378    ) -> Result<()> {
379        // check that we are provided at least one instruction
380        if ctx.accounts.transaction.instruction_index < 1 {
381            // if no instructions were found, mark it as executed and move on
382            ctx.accounts.transaction.set_executed()?;
383            return Ok(());
384        }
385
386        // use for derivation for the authority
387        let ms_key = ctx.accounts.multisig.key();
388
389        // default authority seeds to auth index > 0
390        let authority_seeds = [
391            b"squad",
392            ms_key.as_ref(),
393            &ctx.accounts.transaction.authority_index.to_le_bytes(),
394            b"authority",
395            &[ctx.accounts.transaction.authority_bump],
396        ];
397        // if auth index < 1
398        let ms_authority_seeds = [
399            b"squad",
400            ctx.accounts.multisig.create_key.as_ref(),
401            b"multisig",
402            &[ctx.accounts.multisig.bump],
403        ];
404
405        // unroll account infos from account_list
406        let mapped_remaining_accounts: Vec<AccountInfo> = account_list
407            .iter()
408            .map(|&i| {
409                let index = usize::from(i);
410                ctx.remaining_accounts[index].clone()
411            })
412            .collect();
413
414        // iterator for remaining accounts
415        let ix_iter = &mut mapped_remaining_accounts.iter();
416
417        (1..=ctx.accounts.transaction.instruction_index).try_for_each(|i: u8| {
418            // each ix block starts with the ms_ix account
419            let ms_ix_account: &AccountInfo = next_account_info(ix_iter)?;
420
421            // if the attached instruction doesn't belong to this program, throw error
422            if ms_ix_account.owner != ctx.program_id {
423                return err!(MsError::InvalidInstructionAccount);
424            }
425
426            // deserialize the msIx
427            let mut ix_account_data: &[u8] = &ms_ix_account.try_borrow_mut_data()?;
428            let ms_ix: MsInstruction = MsInstruction::try_deserialize(&mut ix_account_data)?;
429
430            // get the instruction account pda - seeded from transaction account + the transaction accounts instruction index
431            let (ix_pda, _) = Pubkey::find_program_address(
432                &[
433                    b"squad",
434                    ctx.accounts.transaction.key().as_ref(),
435                    &i.to_le_bytes(),
436                    b"instruction",
437                ],
438                ctx.program_id,
439            );
440            // check the instruction account key maches the derived pda
441            if &ix_pda != ms_ix_account.key {
442                return err!(MsError::InvalidInstructionAccount);
443            }
444            // get the instructions program account
445            let ix_program_info: &AccountInfo = next_account_info(ix_iter)?;
446            // check that it matches the submitted account
447            if &ms_ix.program_id != ix_program_info.key {
448                return err!(MsError::InvalidInstructionAccount);
449            }
450
451            let ix_keys = ms_ix.keys.clone();
452            // create the instruction to invoke from the saved ms ix account
453            let ix: Instruction = Instruction::from(ms_ix);
454            // the instruction account vec, with the program account first
455            let mut ix_account_infos: Vec<AccountInfo> = vec![ix_program_info.clone()];
456
457            // loop through the provided remaining accounts
458            for account_index in 0..ix_keys.len() {
459                let ix_account_info = next_account_info(ix_iter)?.clone();
460
461                // check that the ix account keys match the submitted account keys
462                if *ix_account_info.key != ix_keys[account_index].pubkey {
463                    return err!(MsError::InvalidInstructionAccount);
464                }
465
466                ix_account_infos.push(ix_account_info.clone());
467            }
468
469            // execute the ix
470            match ctx.accounts.transaction.authority_index {
471                // if its a 0 authority, use the MS pda seeds
472                0 => {
473                    if &ix.program_id != ctx.program_id {
474                        return err!(MsError::InvalidAuthorityIndex);
475                    }
476                    // Prevent recursive call on execute_transaction/instruction that could create issues
477                    let execute_transaction = Vec::from_hex("e7ad315beb184413").unwrap();
478                    let execute_instruction = Vec::from_hex("301228284b4a936e").unwrap();
479                    if Some(execute_transaction.as_slice()) == ix.data.get(0..8) ||
480                        Some(execute_instruction.as_slice()) == ix.data.get(0..8) {
481                        return err!(MsError::InvalidAuthorityIndex);
482                    }
483
484                    invoke_signed(&ix, &ix_account_infos, &[&ms_authority_seeds])?;
485                }
486                // if its > 1 authority, use the derived authority seeds
487                1.. => {
488                    invoke_signed(&ix, &ix_account_infos, &[&authority_seeds])?;
489                }
490            };
491            Ok(())
492        })?;
493        // set the executed index
494        ctx.accounts.transaction.executed_index = ctx.accounts.transaction.instruction_index;
495        // mark it as executed
496        ctx.accounts.transaction.set_executed()?;
497        // reload any multisig changes
498        ctx.accounts.multisig.reload()?;
499        Ok(())
500    }
501
502    /// Instruction to sequentially execute attached instructions.
503    /// Instructions executed in this matter must be executed in order,
504    /// this may be helpful for processing large batch transfers.
505    /// This instruction can only be used for transactions with an authority
506    /// index of 1 or greater.
507    /// 
508    /// NOTE - do not use this instruction if there is not total clarity around
509    /// potential side effects, as this instruction implies that the approved
510    /// transaction will be executed partially, and potentially spread out over
511    /// a period of time. This could introduce problems with state and failed
512    /// transactions. For example: a program invoked in one of these instructions
513    /// may be upgraded between executions and potentially leave one of the 
514    /// necessary accounts in an invalid state.
515    pub fn execute_instruction<'info>(
516        ctx: Context<'_, '_, '_, 'info, ExecuteInstruction<'info>>,
517    ) -> Result<()> {
518        let ms_key = &ctx.accounts.multisig.key();
519        let ms_ix = &mut ctx.accounts.instruction;
520        let tx = &mut ctx.accounts.transaction;
521
522        // To prevent potential failure with the Squad account auth 0 can't be executed in a specific instruction
523        if tx.authority_index == 0 {
524            return err!(MsError::InvalidAuthorityIndex);
525        }
526
527        // setup the authority seeds
528        let authority_seeds = [
529            b"squad",
530            ms_key.as_ref(),
531            &tx.authority_index.to_le_bytes(),
532            b"authority",
533            &[tx.authority_bump],
534        ];
535
536        // map the saved instruction account data to the instruction to be invoked
537        let ix: Instruction = Instruction {
538            accounts: ms_ix
539                .keys
540                .iter()
541                .map(|k| AccountMeta {
542                    pubkey: k.pubkey,
543                    is_signer: k.is_signer,
544                    is_writable: k.is_writable,
545                })
546                .collect(),
547            data: ms_ix.data.clone(),
548            program_id: ms_ix.program_id,
549        };
550
551        // collect the accounts needed from remaining accounts (order matters)
552        let mut ix_account_infos: Vec<AccountInfo> = Vec::<AccountInfo>::new();
553        let ix_account_iter = &mut ctx.remaining_accounts.iter();
554        // the first account in the submitted list should be the program
555        let ix_program_account = next_account_info(ix_account_iter)?;
556        // check that the programs match
557        if ix_program_account.key != &ix.program_id {
558            return err!(MsError::InvalidInstructionAccount);
559        }
560
561        // loop through the provided remaining accounts - check they match the saved instruction accounts
562        for account_index in 0..ms_ix.keys.len() {
563            let ix_account_info = next_account_info(ix_account_iter)?;
564            // check that the ix account keys match the submitted account keys
565            if ix_account_info.key != &ms_ix.keys[account_index].pubkey {
566                return err!(MsError::InvalidInstructionAccount);
567            }
568            ix_account_infos.push(ix_account_info.clone());
569        }
570
571        if tx.authority_index < 1 && &ix.program_id != ctx.program_id {
572            return err!(MsError::InvalidAuthorityIndex);
573        }
574
575        invoke_signed(&ix, &ix_account_infos, &[&authority_seeds])?;
576
577        // set the executed index to match
578        tx.executed_index = ms_ix.instruction_index;
579        // this is the last instruction - set the transaction as executed
580        if ctx.accounts.instruction.instruction_index == ctx.accounts.transaction.instruction_index
581        {
582            ctx.accounts.transaction.set_executed()?;
583        }
584        Ok(())
585    }
586}