1use 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 pub fn create(
51 ctx: Context<Create>,
52 threshold: u16, create_key: Pubkey, members: Vec<Pubkey>, _meta: String, ) -> Result<()> {
57 let mut members = members;
59 members.sort();
60 members.dedup();
61
62 let total_members = members.len();
64 if total_members < 1 {
65 return err!(MsError::EmptyMembers);
66 }
67
68 if total_members > usize::from(u16::MAX) {
70 return err!(MsError::MaxMembersReached);
71 }
72
73 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 pub fn add_member(ctx: Context<MsAuthRealloc>, new_member: Pubkey) -> Result<()> {
91 if ctx.accounts.multisig.keys.len() >= usize::from(u16::MAX) {
93 return err!(MsError::MaxMembersReached);
94 }
95
96 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 spots_left < 1 {
107 let needed_len = curr_data_size + (10 * 32);
109 AccountInfo::realloc(&multisig_account_info, needed_len, false)?;
111 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 ctx.accounts.multisig.set_change_index(new_index)
124 }
125
126 pub fn remove_member(ctx: Context<MsAuth>, old_member: Pubkey) -> Result<()> {
128 if ctx.accounts.multisig.keys.len() == 1 {
130 return err!(MsError::CannotRemoveSoloMember);
131 }
132 ctx.accounts.multisig.remove_member(old_member)?;
133
134 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 ctx.accounts.multisig.set_change_index(new_index)
142 }
143
144 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 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_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 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 ctx.accounts.multisig.set_change_index(new_index)
191 }
192
193 pub fn change_threshold(ctx: Context<MsAuth>, new_threshold: u16) -> Result<()> {
195 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 ctx.accounts.multisig.set_change_index(new_index)
207 }
208
209 pub fn add_authority(ctx: Context<MsAuth>) -> Result<()> {
215 ctx.accounts.multisig.add_authority()
216 }
217
218 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 pub fn activate_transaction(ctx: Context<ActivateTransaction>) -> Result<()> {
255 ctx.accounts.transaction.activate()
256 }
257
258 pub fn add_instruction(
263 ctx: Context<AddInstruction>,
264 incoming_instruction: IncomingInstruction,
265 ) -> Result<()> {
266 let tx = &mut ctx.accounts.transaction;
267 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 pub fn approve_transaction(ctx: Context<VoteTransaction>) -> Result<()> {
282 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 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 ctx.accounts.transaction.approved.len() >= usize::from(ctx.accounts.multisig.threshold) {
303 ctx.accounts.transaction.ready_to_execute()?;
304 }
305 Ok(())
306 }
307
308 pub fn reject_transaction(ctx: Context<VoteTransaction>) -> Result<()> {
311 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 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 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 pub fn cancel_transaction(ctx: Context<CancelTransaction>) -> Result<()> {
351 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 ctx.accounts.transaction.cancelled.len() >= usize::from(ctx.accounts.multisig.threshold)
363 {
364 ctx.accounts.transaction.set_cancelled()?;
365 }
366 Ok(())
367 }
368
369 pub fn execute_transaction<'info>(
376 ctx: Context<'_, '_, '_, 'info, ExecuteTransaction<'info>>,
377 account_list: Vec<u8>,
378 ) -> Result<()> {
379 if ctx.accounts.transaction.instruction_index < 1 {
381 ctx.accounts.transaction.set_executed()?;
383 return Ok(());
384 }
385
386 let ms_key = ctx.accounts.multisig.key();
388
389 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 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 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 let ix_iter = &mut mapped_remaining_accounts.iter();
416
417 (1..=ctx.accounts.transaction.instruction_index).try_for_each(|i: u8| {
418 let ms_ix_account: &AccountInfo = next_account_info(ix_iter)?;
420
421 if ms_ix_account.owner != ctx.program_id {
423 return err!(MsError::InvalidInstructionAccount);
424 }
425
426 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 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 if &ix_pda != ms_ix_account.key {
442 return err!(MsError::InvalidInstructionAccount);
443 }
444 let ix_program_info: &AccountInfo = next_account_info(ix_iter)?;
446 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 let ix: Instruction = Instruction::from(ms_ix);
454 let mut ix_account_infos: Vec<AccountInfo> = vec![ix_program_info.clone()];
456
457 for account_index in 0..ix_keys.len() {
459 let ix_account_info = next_account_info(ix_iter)?.clone();
460
461 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 match ctx.accounts.transaction.authority_index {
471 0 => {
473 if &ix.program_id != ctx.program_id {
474 return err!(MsError::InvalidAuthorityIndex);
475 }
476 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 1.. => {
488 invoke_signed(&ix, &ix_account_infos, &[&authority_seeds])?;
489 }
490 };
491 Ok(())
492 })?;
493 ctx.accounts.transaction.executed_index = ctx.accounts.transaction.instruction_index;
495 ctx.accounts.transaction.set_executed()?;
497 ctx.accounts.multisig.reload()?;
499 Ok(())
500 }
501
502 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 if tx.authority_index == 0 {
524 return err!(MsError::InvalidAuthorityIndex);
525 }
526
527 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 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 let mut ix_account_infos: Vec<AccountInfo> = Vec::<AccountInfo>::new();
553 let ix_account_iter = &mut ctx.remaining_accounts.iter();
554 let ix_program_account = next_account_info(ix_account_iter)?;
556 if ix_program_account.key != &ix.program_id {
558 return err!(MsError::InvalidInstructionAccount);
559 }
560
561 for account_index in 0..ms_ix.keys.len() {
563 let ix_account_info = next_account_info(ix_account_iter)?;
564 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 tx.executed_index = ms_ix.instruction_index;
579 if ctx.accounts.instruction.instruction_index == ctx.accounts.transaction.instruction_index
581 {
582 ctx.accounts.transaction.set_executed()?;
583 }
584 Ok(())
585 }
586}