multisig_lite/lib.rs
1//! A native [SOL] multisig on-chain program.
2//!
3//! [sol]: https://solana.com/
4//!
5//! # Examples
6//!
7//! Here is how to create a new multisig account on-chain.
8//!
9//! Please refer to the [`multisig_lite`] module level
10//! documentation for the other instructions' example.
11//!
12//! ```no_run
13//! use std::rc::Rc;
14//!
15//! use solana_sdk::commitment_config::CommitmentConfig;
16//! use solana_sdk::pubkey::Pubkey;
17//! use solana_sdk::signature::read_keypair_file;
18//! use solana_sdk::signer::Signer;
19//! use solana_sdk::system_program;
20//!
21//! use anchor_client::{Client, Cluster};
22//!
23//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
24//! let url = Cluster::Devnet;
25//! let funder = Rc::new(read_keypair_file(
26//! shellexpand::tilde("~/.config/solana/id.json").as_ref(),
27//! )?);
28//! let opts = CommitmentConfig::processed();
29//! let pid = multisig_lite::id();
30//! let program = Client::new_with_options(url, funder.clone(), opts).program(pid);
31//!
32//! // Gets the PDAs.
33//! let (state_pda, state_bump) =
34//! Pubkey::find_program_address(&[b"state", funder.pubkey().as_ref()], &pid);
35//! let (fund_pda, fund_bump) = Pubkey::find_program_address(&[b"fund", state_pda.as_ref()], &pid);
36//!
37//! // Creates a multisig account.
38//! let sig = program
39//! .request()
40//! .accounts(multisig_lite::accounts::Create {
41//! funder: funder.pubkey(),
42//! state: state_pda,
43//! fund: fund_pda,
44//! system_program: system_program::id(),
45//! })
46//! .args(multisig_lite::instruction::Create {
47//! m: 2, // m as in m/n.
48//! signers: vec![funder.pubkey(), Pubkey::new_unique(), Pubkey::new_unique()],
49//! q: 10, // transfer queue limit.
50//! _state_bump: state_bump,
51//! fund_bump,
52//! })
53//! .signer(funder.as_ref())
54//! .send()?;
55//!
56//! println!("{sig}");
57//! # Ok(())
58//! # }
59//! ```
60
61use std::collections::{HashMap, HashSet};
62use std::ops::DerefMut;
63
64use anchor_lang::prelude::*;
65use anchor_lang::solana_program::program::{invoke, invoke_signed};
66use anchor_lang::solana_program::system_instruction;
67
68#[cfg(not(feature = "localnet"))]
69declare_id!("Ecycmji8eeggXrA3rD2cdEHpHDnP4btvVfcyTBS9cG9t");
70#[cfg(feature = "localnet")]
71declare_id!("AeAQKcvUbG6LmunEAiL2Vim5dN2uL5TNwJfgsGdyroQ3");
72
73/// A multisig program specific error code.
74#[error_code]
75pub enum Error {
76 /// Multisig [`State`] queue is empty.
77 #[msg("Multisig account is empty. Please create transactions")]
78 AccountEmpty,
79
80 /// Multisig [`State`] queue is full.
81 #[msg("Multisig transaction queue is full. Please approve those.")]
82 AccountFull,
83
84 /// Multisig [`State`] account is locked.
85 #[msg("Multisig account is locked. Please approve the transactions")]
86 AccountLocked,
87
88 /// Missing transfer recipient AccountInfo.
89 #[msg("Missing transfer recipient AccountInfo")]
90 MissingRecipientAccountInfo,
91
92 /// Multisig `Fund` account is not writable.
93 #[msg("Fund account is not writable")]
94 FundAccountNotWritable,
95
96 /// Multisig `Fund` account data is not empty")]
97 #[msg("Fund account data is not empty")]
98 FundAccountIsNotEmpty,
99
100 /// Invalid Multisig `Fund` PDA.
101 #[msg("Invalid fund account")]
102 InvalidFundAddress,
103
104 /// Invalid Multisig `Fund` account bump.
105 #[msg("Invalid fund bump seed")]
106 InvalidFundBumpSeed,
107
108 /// No signers.
109 #[msg("No signers provided")]
110 NoSigners,
111
112 /// Too many signers provided.
113 #[msg("Too many signers provided")]
114 TooManySigners,
115
116 /// The threshold, e.g. `m`, is too high.
117 #[msg("Threshold too high")]
118 ThresholdTooHigh,
119
120 /// Invalid signer given.
121 #[msg("Invalid signer")]
122 InvalidSigner,
123
124 /// Not enough `Fund` balance.
125 #[msg("There is not enough fund balance")]
126 NotEnoughFundBalance,
127}
128
129/// A multisig [`State`] PDA account data.
130///
131/// # Examples
132///
133/// Here is how to query the [`State`] PDA account on Devnet.
134///
135/// ```no_run
136/// use std::rc::Rc;
137///
138/// use solana_sdk::commitment_config::CommitmentConfig;
139/// use solana_sdk::pubkey::Pubkey;
140/// use solana_sdk::signature::read_keypair_file;
141/// use solana_sdk::signer::Signer;
142///
143/// use anchor_client::{Client, Cluster};
144///
145/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
146/// let url = Cluster::Devnet;
147/// let funder = Rc::new(read_keypair_file(
148/// shellexpand::tilde("~/.config/solana/id.json").as_ref(),
149/// )?);
150/// let opts = CommitmentConfig::processed();
151/// let pid = multisig_lite::id();
152/// let program = Client::new_with_options(url, funder.clone(), opts).program(pid);
153///
154/// // Gets the PDAs.
155/// let (state_pda, _state_bump) =
156/// Pubkey::find_program_address(&[b"state", funder.pubkey().as_ref()], &pid);
157///
158/// // Query the `multisig_lite::State` account.
159/// let state: multisig_lite::State = program.account(state_pda)?;
160///
161/// // Print out the state account.
162/// println!("{state:?}");
163/// # Ok(())
164/// # }
165/// ```
166#[account]
167#[derive(Debug)]
168pub struct State {
169 /// A threshold.
170 pub m: u8,
171
172 /// An array of signers Pubkey.
173 pub signers: Vec<Pubkey>,
174
175 /// A current signed state.
176 pub signed: Vec<bool>,
177
178 /// A fund PDA account, holding the native SOL.
179 pub fund: Pubkey,
180
181 /// A balance of the fund in lamports.
182 pub balance: u64,
183
184 /// A limit of the pending transactions.
185 pub q: u8,
186
187 /// An array of the pending transactions.
188 pub queue: Vec<Pubkey>,
189}
190
191impl State {
192 /// A minimum signers.
193 const MIN_SIGNERS: u8 = 1;
194
195 /// A maximum signers.
196 const MAX_SIGNERS: u8 = u8::MAX;
197
198 /// A maximum transaction queue.
199 const MIN_QUEUE: u8 = 1;
200
201 /// A maximum transaction queue.
202 const MAX_QUEUE: u8 = u8::MAX;
203
204 fn space(signers: &[Pubkey], q: u8) -> usize {
205 let n = Self::valid_n(signers.len() as u8) as usize;
206 let q = Self::valid_q(q) as usize;
207 8 + 1 + 4 + 32 * n + 4 + n + 32 + 8 + 1 + 4 + 32 * q
208 }
209
210 /// Returns the valid n, number of signers.
211 fn valid_n(n: u8) -> u8 {
212 n.clamp(Self::MIN_SIGNERS, Self::MAX_SIGNERS)
213 }
214
215 /// Returns the valid q, queue length.
216 fn valid_q(q: u8) -> u8 {
217 q.clamp(Self::MIN_QUEUE, Self::MAX_QUEUE)
218 }
219
220 /// Checks if the transfer queue is empty.
221 fn is_queue_empty(&self) -> bool {
222 self.queue.is_empty()
223 }
224
225 /// Check if the multisig queue is full.
226 fn is_queue_full(&self) -> bool {
227 self.queue.len() == self.q as usize
228 }
229
230 /// Checks if the account had been locked.
231 ///
232 /// The multisig account is locked once it's signed
233 /// by anyone. It will be unlocked once the current
234 /// pending transactions were completed.
235 fn is_locked(&self) -> bool {
236 self.signed.iter().any(|signed| *signed)
237 }
238
239 /// Validates the multisig queue.
240 #[allow(clippy::result_large_err)]
241 fn validate_queue(&self) -> Result<()> {
242 require!(!self.is_queue_full(), Error::AccountFull);
243 Ok(())
244 }
245
246 /// Validates the multisig fund account.
247 #[allow(clippy::result_large_err)]
248 fn validate_fund<'info>(
249 state: &Account<'info, Self>,
250 fund: &UncheckedAccount<'info>,
251 bump: u8,
252 ) -> Result<()> {
253 if !fund.is_writable {
254 Err(Error::FundAccountNotWritable)?;
255 }
256 if !fund.data_is_empty() {
257 Err(Error::FundAccountIsNotEmpty)?;
258 }
259 let state_key = state.key();
260 let seed = [b"fund", state_key.as_ref(), &[bump]];
261 let pda = match Pubkey::create_program_address(&seed, &id()) {
262 Err(_e) => Err(Error::InvalidFundBumpSeed)?,
263 Ok(pda) => pda,
264 };
265 require_keys_eq!(pda, fund.key(), Error::InvalidFundAddress);
266
267 Ok(())
268 }
269
270 /// Creates a fund account.
271 #[allow(clippy::result_large_err)]
272 fn create_fund_account<'info>(
273 state: &Account<'info, Self>,
274 fund: &UncheckedAccount<'info>,
275 funder: &Signer<'info>,
276 bump: u8,
277 ) -> Result<()> {
278 let lamports = Rent::get()?.minimum_balance(0);
279 let ix = system_instruction::create_account(&funder.key(), &fund.key(), lamports, 0, &id());
280 let state_key = state.key();
281 let accounts = [funder.to_account_info(), fund.to_account_info()];
282 let seed = [b"fund", state_key.as_ref(), &[bump]];
283
284 // CPI.
285 invoke_signed(&ix, &accounts, &[&seed])?;
286
287 Ok(())
288 }
289
290 /// Withdraw fund.
291 #[allow(clippy::result_large_err)]
292 fn transfer_fund(
293 _state: &Account<'_, Self>,
294 from: &AccountInfo<'_>,
295 to: &AccountInfo<'_>,
296 lamports: u64,
297 _bump: u8,
298 ) -> Result<()> {
299 // The following code hit the runtime error, [`InstructionError::ExternalLamportSpend`].
300 // Instead, we'll transfer the lamports natively,
301 // as suggested by the [Solana cookbook].
302 //
303 // [`InstructionError::ExternalLamportSpend`]: https://docs.rs/solana-program/latest/solana_program/instruction/enum.InstructionError.html#variant.ExternalAccountLamportSpend
304 // [solana cookbook]: https://solanacookbook.com/references/programs.html#how-to-transfer-sol-in-a-program
305
306 /*
307 let ix = system_instruction::transfer(from.key, &to.key, lamports);
308 let accounts = [from, to];
309 let state_key = state.key();
310 let seed = [b"fund", state_key.as_ref(), &[bump]];
311 invoke_signed(
312 &ix,
313 &accounts,
314 &[&seed],
315 )?;
316 */
317 **from.try_borrow_mut_lamports()? -= lamports;
318 **to.try_borrow_mut_lamports()? += lamports;
319
320 Ok(())
321 }
322}
323
324/// A multisig [`Transfer`] account data.
325///
326/// # Examples
327///
328/// Here is how to query the [`Transfer`] PDA account on Devnet.
329///
330/// ```no_run
331/// use std::rc::Rc;
332///
333/// use solana_sdk::commitment_config::CommitmentConfig;
334/// use solana_sdk::pubkey::Pubkey;
335/// use solana_sdk::signature::read_keypair_file;
336/// use solana_sdk::signer::Signer;
337///
338/// use anchor_client::{Client, Cluster};
339///
340/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
341/// let url = Cluster::Devnet;
342/// let funder = Rc::new(read_keypair_file(
343/// shellexpand::tilde("~/.config/solana/id.json").as_ref(),
344/// )?);
345/// let opts = CommitmentConfig::processed();
346/// let pid = multisig_lite::id();
347/// let program = Client::new_with_options(url, funder.clone(), opts).program(pid);
348///
349/// // Gets the PDAs.
350/// let (state_pda, _state_bump) =
351/// Pubkey::find_program_address(&[b"state", funder.pubkey().as_ref()], &pid);
352///
353/// // Query the `multisig_lite::State` account to get the queued transfers.
354/// let state: multisig_lite::State = program.account(state_pda)?;
355///
356/// // Query the `multisig_lite::Transfer` accounts iteratively.
357/// for transfer in state.queue {
358/// let transfer: multisig_lite::Transfer = program.account(transfer)?;
359/// println!("{transfer:?}");
360/// }
361/// # Ok(())
362/// # }
363/// ```
364#[account]
365#[derive(Debug)]
366pub struct Transfer {
367 /// An creator of the transfer, one of the multisig
368 /// signers.
369 pub creator: Pubkey,
370
371 /// A recipient of the transfer.
372 pub recipient: Pubkey,
373
374 /// A lamports to transfer.
375 pub lamports: u64,
376}
377
378impl Transfer {
379 const SPACE: usize = 8 + 32 + 32 + 8;
380}
381
382/// Accounts for the [`multisig_lite::create`] instruction handler.
383///
384/// Please refer to the [`multisig_lite::create`] document for the example.
385#[derive(Accounts)]
386#[instruction(m: u8, signers: Vec<Pubkey>, q: u8, state_bump: u8, fund_bump: u8)]
387pub struct Create<'info> {
388 /// A funder of the multisig account.
389 #[account(mut)]
390 pub funder: Signer<'info>,
391
392 /// A multisig state PDA account.
393 #[account(
394 init,
395 payer = funder,
396 space = State::space(&signers, q),
397 seeds = [b"state", funder.key.as_ref()],
398 bump,
399 )]
400 pub state: Account<'info, State>,
401
402 /// A multisig fund account.
403 ///
404 /// CHECK: Checked by the [`multisig_lite::create`] instruction handler.
405 #[account(mut, seeds = [b"fund", state.key().as_ref()], bump = fund_bump)]
406 pub fund: UncheckedAccount<'info>,
407
408 /// The system program to create a multisig PDA accounts.
409 pub system_program: Program<'info, System>,
410}
411
412/// Accounts for the [`multisig_lite::fund`] instruction handler.
413///
414/// Please refer to the [`multisig_lite::fund`] document for the example.
415#[derive(Accounts)]
416#[instruction(lamports: u64, state_bump: u8, fund_bump: u8)]
417pub struct Fund<'info> {
418 /// A funder of the account.
419 ///
420 /// The funding is only allowed by the multisig account creator.
421 #[account(mut)]
422 pub funder: Signer<'info>,
423
424 /// A multisig state PDA account.
425 #[account(mut, seeds = [b"state", funder.key.as_ref()], bump = state_bump)]
426 pub state: Box<Account<'info, State>>,
427
428 /// A multisig fund account.
429 ///
430 /// CHECK: Checked by the [`multisig_lite::fund`] instruction handler.
431 #[account(mut, seeds = [b"fund", state.key().as_ref()], bump = fund_bump)]
432 pub fund: UncheckedAccount<'info>,
433
434 /// The system program to make the transfer of the fund.
435 pub system_program: Program<'info, System>,
436}
437
438/// Accounts for the [`multisig_lite::create_transfer`] instruction handler.
439///
440/// Please refer to the [`multisig_lite::create_transfer`] document for the example.
441#[derive(Accounts)]
442#[instruction(recipient: Pubkey, lamports: u64, fund_bump: u8)]
443pub struct CreateTransfer<'info> {
444 /// An initiator of the fund transfer.
445 ///
446 /// It should be one of the signers of the multisig account.
447 #[account(mut)]
448 pub creator: Signer<'info>,
449
450 /// A multisig state PDA account.
451 #[account(mut)]
452 pub state: Box<Account<'info, State>>,
453
454 /// A multisig fund PDA account.
455 ///
456 /// CHECK: Checked by the [`multisig_lite::create_transfer`] instruction handler.
457 #[account(mut, seeds = [b"fund", state.key().as_ref()], bump = fund_bump)]
458 pub fund: UncheckedAccount<'info>,
459
460 /// A transfer account to keep the queued transfer info.
461 #[account(init, payer = creator, space = Transfer::SPACE)]
462 pub transfer: Box<Account<'info, Transfer>>,
463
464 /// The system program to create a transfer account.
465 pub system_program: Program<'info, System>,
466}
467
468/// Accounts for the [`multisig_lite::approve`] instruction handler.
469///
470/// Once one of the signer approves, the account is locked
471/// for the new transfer unless:
472///
473/// 1) Meets the m number of signers approval.
474/// 2) Closes the account.
475///
476/// In case of the 1 above, the account will be unlocked
477/// and starts to take a new transfer again.
478///
479/// Please refer to the [`multisig_lite::approve`] document for the example.
480#[derive(Accounts)]
481#[instruction(fund_bump: u8)]
482pub struct Approve<'info> {
483 /// An approver of the current state of the multisg account.
484 #[account(mut)]
485 pub signer: Signer<'info>,
486
487 /// A multisig state PDA account.
488 #[account(mut)]
489 pub state: Box<Account<'info, State>>,
490
491 /// A multisig fund account.
492 ///
493 /// CHECK: Checked by the [`multisig_lite::approve`] instruction handler.
494 #[account(mut, seeds = [b"fund", state.key().as_ref()], bump = fund_bump)]
495 pub fund: UncheckedAccount<'info>,
496}
497
498/// Accounts for the [`multisig_lite::close`] instruction handler.
499///
500/// Please refer to the [`multisig_lite::close`] document for the example.
501#[derive(Accounts)]
502#[instruction(state_bump: u8, fund_bump: u8)]
503pub struct Close<'info> {
504 /// An original funder of the multisig account.
505 #[account(mut)]
506 pub funder: Signer<'info>,
507
508 /// A multisig state PDA account.
509 #[account(mut, close = funder, seeds = [b"state", funder.key.as_ref()], bump = state_bump)]
510 pub state: Box<Account<'info, State>>,
511
512 /// A multisig fund PDA account.
513 ///
514 /// CHECK: Checked by the [`multisig_lite::close`] instruction handler.
515 #[account(mut, seeds = [b"fund", state.key().as_ref()], bump = fund_bump)]
516 pub fund: UncheckedAccount<'info>,
517}
518
519/// Module representing the program instruction handlers.
520///
521/// # Examples
522///
523/// Here is how to approve pending transfers on Devnet.
524///
525/// Please take a look at the individual functions for other
526/// instruction opperations. e.g. [`multisig_lite::fund`] for
527/// how to fund the multisig account.
528///
529/// ```no_run
530/// use std::rc::Rc;
531///
532/// use solana_sdk::commitment_config::CommitmentConfig;
533/// use solana_sdk::instruction::AccountMeta;
534/// use solana_sdk::pubkey::Pubkey;
535/// use solana_sdk::signature::read_keypair_file;
536/// use solana_sdk::signer::Signer;
537///
538/// use anchor_client::{Client, Cluster};
539///
540/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
541/// let url = Cluster::Devnet;
542/// let signer = Rc::new(read_keypair_file(
543/// shellexpand::tilde("~/.config/solana/id.json").as_ref(),
544/// )?);
545/// let opts = CommitmentConfig::processed();
546/// let pid = multisig_lite::id();
547/// let program = Client::new_with_options(url, signer.clone(), opts).program(pid);
548///
549/// // Gets the PDAs.
550/// let (state_pda, state_bump) =
551/// Pubkey::find_program_address(&[b"state", signer.pubkey().as_ref()], &pid);
552/// let (fund_pda, fund_bump) = Pubkey::find_program_address(&[b"fund", state_pda.as_ref()], &pid);
553///
554/// // Gets the pending transfers and the recipients account info.
555/// let mut remaining_accounts = vec![];
556/// let state: multisig_lite::State = program.account(state_pda)?;
557/// for transfer_pubkey in state.queue {
558/// let transfer: multisig_lite::Transfer = program.account(transfer_pubkey)?;
559///
560/// // Pushes the transfer account.
561/// remaining_accounts.push(AccountMeta {
562/// pubkey: transfer_pubkey,
563/// is_signer: false,
564/// is_writable: true,
565/// });
566///
567/// // Pushes the recipient account.
568/// remaining_accounts.push(AccountMeta {
569/// pubkey: transfer.recipient,
570/// is_signer: false,
571/// is_writable: true,
572/// });
573/// }
574///
575/// // Approve the multisig account.
576/// let sig = program
577/// .request()
578/// .accounts(multisig_lite::accounts::Approve {
579/// signer: signer.pubkey(),
580/// state: state_pda,
581/// fund: fund_pda,
582/// })
583/// .args(multisig_lite::instruction::Approve { fund_bump })
584/// .accounts(remaining_accounts)
585/// .signer(signer.as_ref())
586/// .send()?;
587///
588/// println!("{sig}");
589/// # Ok(())
590/// # }
591/// ```
592#[program]
593pub mod multisig_lite {
594 use super::*;
595
596 /// Creates a multisig account.
597 ///
598 /// It's restricted one multisig account to each funder Pubkey,
599 /// as it's used for the multisig PDA address generation.
600 ///
601 /// # Examples
602 ///
603 /// Here is how you create a multisig account on Devnet:
604 ///
605 /// ```no_run
606 /// use std::rc::Rc;
607 ///
608 /// use solana_sdk::commitment_config::CommitmentConfig;
609 /// use solana_sdk::pubkey::Pubkey;
610 /// use solana_sdk::signature::read_keypair_file;
611 /// use solana_sdk::signer::Signer;
612 /// use solana_sdk::system_program;
613 ///
614 /// use anchor_client::{Client, Cluster};
615 ///
616 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
617 /// let url = Cluster::Devnet;
618 /// let funder = Rc::new(read_keypair_file(
619 /// shellexpand::tilde("~/.config/solana/id.json").as_ref(),
620 /// )?);
621 /// let opts = CommitmentConfig::processed();
622 /// let pid = multisig_lite::id();
623 /// let program = Client::new_with_options(url, funder.clone(), opts).program(pid);
624 ///
625 /// // Gets the PDAs.
626 /// let (state_pda, state_bump) =
627 /// Pubkey::find_program_address(&[b"state", funder.pubkey().as_ref()], &pid);
628 /// let (fund_pda, fund_bump) = Pubkey::find_program_address(&[b"fund", state_pda.as_ref()], &pid);
629 ///
630 /// // Creates a multisig account.
631 /// let sig = program
632 /// .request()
633 /// .accounts(multisig_lite::accounts::Create {
634 /// funder: funder.pubkey(),
635 /// state: state_pda,
636 /// fund: fund_pda,
637 /// system_program: system_program::id(),
638 /// })
639 /// .args(multisig_lite::instruction::Create {
640 /// m: 2, // m as in m/n.
641 /// signers: vec![funder.pubkey(), Pubkey::new_unique(), Pubkey::new_unique()],
642 /// q: 10, // transfer queue limit.
643 /// _state_bump: state_bump,
644 /// fund_bump,
645 /// })
646 /// .signer(funder.as_ref())
647 /// .send()?;
648 ///
649 /// println!("{sig}");
650 /// # Ok(())
651 /// # }
652 /// ```
653 #[allow(clippy::result_large_err)]
654 pub fn create(
655 ctx: Context<Create>,
656 m: u8,
657 signers: Vec<Pubkey>,
658 q: u8,
659 _state_bump: u8,
660 fund_bump: u8,
661 ) -> Result<()> {
662 let funder = &mut ctx.accounts.funder;
663 let state = &mut ctx.accounts.state;
664 let fund = &mut ctx.accounts.fund;
665
666 // At least one signer is required.
667 require_gte!(m, State::MIN_SIGNERS, Error::NoSigners);
668
669 // Validate the multisig fund account.
670 State::validate_fund(state, fund, fund_bump)?;
671
672 // Checks the uniqueness of signer's address.
673 let signers: HashSet<_> = signers.into_iter().collect();
674 require_gte!(signers.len(), State::MIN_SIGNERS as usize, Error::NoSigners);
675 require_gte!(
676 State::MAX_SIGNERS as usize,
677 signers.len(),
678 Error::TooManySigners
679 );
680
681 let threshold = m as usize;
682 require_gte!(signers.len(), threshold, Error::ThresholdTooHigh);
683
684 // Creates a fund account.
685 State::create_fund_account(state, fund, funder, fund_bump)?;
686
687 // Initializes the multisig state account.
688 state.m = m;
689 state.signers = signers.into_iter().collect();
690 state.signed = vec![false; state.signers.len()];
691 state.fund = fund.key();
692 state.balance = 0;
693 state.q = State::valid_q(q);
694
695 Ok(())
696 }
697
698 /// Funds lamports to the multisig fund account.
699 ///
700 /// The funding is only allowed by the multisig account funder.
701 ///
702 /// # Examples
703 ///
704 /// Here is how to fund to the multisig account on Devnet:
705 ///
706 /// ```no_run
707 /// use std::rc::Rc;
708 ///
709 /// use solana_sdk::commitment_config::CommitmentConfig;
710 /// use solana_sdk::pubkey::Pubkey;
711 /// use solana_sdk::native_token::LAMPORTS_PER_SOL;
712 /// use solana_sdk::signature::read_keypair_file;
713 /// use solana_sdk::signer::Signer;
714 /// use solana_sdk::system_program;
715 ///
716 /// use anchor_client::{Client, Cluster};
717 ///
718 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
719 /// let url = Cluster::Devnet;
720 /// let funder = Rc::new(read_keypair_file(
721 /// shellexpand::tilde("~/.config/solana/id.json").as_ref(),
722 /// )?);
723 /// let opts = CommitmentConfig::processed();
724 /// let pid = multisig_lite::id();
725 /// let program = Client::new_with_options(url, funder.clone(), opts).program(pid);
726 ///
727 /// // Gets the PDAs.
728 /// let (state_pda, state_bump) =
729 /// Pubkey::find_program_address(&[b"state", funder.pubkey().as_ref()], &pid);
730 /// let (fund_pda, fund_bump) = Pubkey::find_program_address(&[b"fund", state_pda.as_ref()], &pid);
731 ///
732 /// // Funds the multisig account.
733 /// let sig = program
734 /// .request()
735 /// .accounts(multisig_lite::accounts::Fund {
736 /// funder: funder.pubkey(),
737 /// state: state_pda,
738 /// fund: fund_pda,
739 /// system_program: system_program::id(),
740 /// })
741 /// .args(multisig_lite::instruction::Fund {
742 /// lamports: 1_000_000 * LAMPORTS_PER_SOL, // 1M SOL!? :)
743 /// _state_bump: state_bump,
744 /// fund_bump,
745 /// })
746 /// .signer(funder.as_ref())
747 /// .send()?;
748 ///
749 /// println!("{sig}");
750 /// # Ok(())
751 /// # }
752 /// ```
753 #[allow(clippy::result_large_err)]
754 pub fn fund(ctx: Context<Fund>, lamports: u64, _state_bump: u8, fund_bump: u8) -> Result<()> {
755 let funder = &ctx.accounts.funder;
756 let state = &mut ctx.accounts.state;
757 let fund = &mut ctx.accounts.fund;
758
759 // Validate the multisig fund account.
760 State::validate_fund(state, fund, fund_bump)?;
761
762 // CPI to transfer fund to the multisig fund account.
763 let ix = system_instruction::transfer(&funder.key(), &fund.key(), lamports);
764 let accounts = [funder.to_account_info(), fund.to_account_info()];
765 invoke(&ix, &accounts)?;
766
767 // Update the balance.
768 state.balance += lamports;
769
770 Ok(())
771 }
772
773 /// Creates a queued transfer lamports to the recipient.
774 ///
775 /// Transfer account creation fee will be given back to the
776 /// creator of the transfer from the multisig fund.
777 ///
778 /// # Examples
779 ///
780 /// Here is how to create a pending transfer on Devnet:
781 ///
782 /// ```no_run
783 /// use std::rc::Rc;
784 ///
785 /// use solana_sdk::commitment_config::CommitmentConfig;
786 /// use solana_sdk::native_token::LAMPORTS_PER_SOL;
787 /// use solana_sdk::pubkey::Pubkey;
788 /// use solana_sdk::signature::read_keypair_file;
789 /// use solana_sdk::signer::{keypair::Keypair, Signer};
790 /// use solana_sdk::system_program;
791 ///
792 /// use anchor_client::{Client, Cluster};
793 ///
794 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
795 /// let url = Cluster::Devnet;
796 /// let funder = Rc::new(read_keypair_file(
797 /// shellexpand::tilde("~/.config/solana/id.json").as_ref(),
798 /// )?);
799 /// let opts = CommitmentConfig::processed();
800 /// let pid = multisig_lite::id();
801 /// let program = Client::new_with_options(url, funder.clone(), opts).program(pid);
802 ///
803 /// // Gets the PDAs.
804 /// let (state_pda, state_bump) =
805 /// Pubkey::find_program_address(&[b"state", funder.pubkey().as_ref()], &pid);
806 /// let (fund_pda, fund_bump) = Pubkey::find_program_address(&[b"fund", state_pda.as_ref()], &pid);
807 ///
808 /// // Temporary transfer keypair.
809 /// //
810 /// // This is only required for the transaction signature and
811 /// // won't be required once the transaction is recorded on the
812 /// // ledger.
813 /// let transfer = Keypair::new();
814 ///
815 /// // Creates a pending transfer.
816 /// let sig = program
817 /// .request()
818 /// .accounts(multisig_lite::accounts::CreateTransfer {
819 /// creator: funder.pubkey(),
820 /// state: state_pda,
821 /// fund: fund_pda,
822 /// transfer: transfer.pubkey(),
823 /// system_program: system_program::id(),
824 /// })
825 /// .args(multisig_lite::instruction::CreateTransfer {
826 /// recipient: Pubkey::new_unique(),
827 /// lamports: 1_000_000 * LAMPORTS_PER_SOL, // 1M SOL!? :)
828 /// fund_bump,
829 /// })
830 /// .signer(funder.as_ref())
831 /// .signer(&transfer)
832 /// .send()?;
833 ///
834 /// println!("{sig}");
835 /// # Ok(())
836 /// # }
837 /// ```
838 #[allow(clippy::result_large_err)]
839 pub fn create_transfer(
840 ctx: Context<CreateTransfer>,
841 recipient: Pubkey,
842 lamports: u64,
843 fund_bump: u8,
844 ) -> Result<()> {
845 let creator = &ctx.accounts.creator;
846 let state = &mut ctx.accounts.state;
847 let fund = &mut ctx.accounts.fund;
848 let transfer = &mut ctx.accounts.transfer;
849
850 // Checks if the account is locked.
851 require!(!state.is_locked(), Error::AccountLocked);
852
853 // Validate the multisig fund account.
854 State::validate_fund(state, fund, fund_bump)?;
855
856 // Checks the creator.
857 let creator_key = creator.key();
858 let signers = &state.signers;
859 require!(signers.contains(&creator_key), Error::InvalidSigner);
860
861 // Check the current transfer queue.
862 state.validate_queue()?;
863
864 // Checks the multisig fund balance.
865 require_gte!(state.balance, lamports, Error::NotEnoughFundBalance);
866
867 // Giving back the rent fee to the creator.
868 let from = fund.to_account_info();
869 let to = creator.to_account_info();
870 let rent = transfer.to_account_info().lamports();
871 State::transfer_fund(state, &from, &to, rent, fund_bump)?;
872
873 // Initializes the transfer account, and
874 // queue it under multisig account for the
875 // future transfer execution.
876 transfer.creator = creator_key;
877 transfer.recipient = recipient;
878 transfer.lamports = lamports;
879 state.balance -= lamports;
880 state.queue.push(transfer.key());
881
882 Ok(())
883 }
884
885 /// Approves the transactions and executes the transfer
886 /// in case the `m` approvals are met.
887 ///
888 /// # Examples
889 ///
890 /// Here is how to approve pending transfers on Devnet:
891 ///
892 /// ```no_run
893 /// use std::rc::Rc;
894 ///
895 /// use solana_sdk::commitment_config::CommitmentConfig;
896 /// use solana_sdk::instruction::AccountMeta;
897 /// use solana_sdk::pubkey::Pubkey;
898 /// use solana_sdk::signature::read_keypair_file;
899 /// use solana_sdk::signer::Signer;
900 ///
901 /// use anchor_client::{Client, Cluster};
902 ///
903 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
904 /// let url = Cluster::Devnet;
905 /// let signer = Rc::new(read_keypair_file(
906 /// shellexpand::tilde("~/.config/solana/id.json").as_ref(),
907 /// )?);
908 /// let opts = CommitmentConfig::processed();
909 /// let pid = multisig_lite::id();
910 /// let program = Client::new_with_options(url, signer.clone(), opts).program(pid);
911 ///
912 /// // Gets the PDAs.
913 /// let (state_pda, state_bump) =
914 /// Pubkey::find_program_address(&[b"state", signer.pubkey().as_ref()], &pid);
915 /// let (fund_pda, fund_bump) = Pubkey::find_program_address(&[b"fund", state_pda.as_ref()], &pid);
916 ///
917 /// // Gets the pending transfers and the recipients account info.
918 /// let mut remaining_accounts = vec![];
919 /// let state: multisig_lite::State = program.account(state_pda)?;
920 /// for transfer_pubkey in state.queue {
921 /// let transfer: multisig_lite::Transfer = program.account(transfer_pubkey)?;
922 ///
923 /// // Pushes the transfer account.
924 /// remaining_accounts.push(AccountMeta {
925 /// pubkey: transfer_pubkey,
926 /// is_signer: false,
927 /// is_writable: true,
928 /// });
929 ///
930 /// // Pushes the recipient account.
931 /// remaining_accounts.push(AccountMeta {
932 /// pubkey: transfer.recipient,
933 /// is_signer: false,
934 /// is_writable: true,
935 /// });
936 /// }
937 ///
938 /// // Approve the multisig account.
939 /// let sig = program
940 /// .request()
941 /// .accounts(multisig_lite::accounts::Approve {
942 /// signer: signer.pubkey(),
943 /// state: state_pda,
944 /// fund: fund_pda,
945 /// })
946 /// .args(multisig_lite::instruction::Approve { fund_bump })
947 /// .accounts(remaining_accounts)
948 /// .signer(signer.as_ref())
949 /// .send()?;
950 ///
951 /// println!("{sig}");
952 /// # Ok(())
953 /// # }
954 /// ```
955 #[allow(clippy::result_large_err)]
956 pub fn approve(ctx: Context<Approve>, fund_bump: u8) -> Result<()> {
957 let signer = &ctx.accounts.signer;
958 let state = &mut ctx.accounts.state;
959 let fund = &mut ctx.accounts.fund;
960 let remaining_accounts: HashMap<_, _> = ctx
961 .remaining_accounts
962 .iter()
963 .map(|account| (account.key, account))
964 .collect();
965
966 // Validate the multisig fund account.
967 State::validate_fund(state, fund, fund_bump)?;
968
969 // Nothing to approve.
970 require!(!state.is_queue_empty(), Error::AccountEmpty);
971
972 // Checks the signer.
973 let signer_key = signer.key();
974 let signers = &state.signers;
975 let signer_index = match signers.iter().position(|pubkey| *pubkey == signer_key) {
976 None => return Err(Error::InvalidSigner.into()),
977 Some(signer_index) => signer_index,
978 };
979
980 // Due to the single transaction limitation, we allow the multiple approval
981 // so that we take care of the transfer in batch.
982 if !state.signed[signer_index] {
983 state.signed[signer_index] = true;
984 }
985
986 // Checks the threshold.
987 let signed = state.signed.iter().filter(|&signed| *signed).count() as u8;
988 if signed < state.m {
989 return Ok(());
990 }
991
992 // Finds out the executable transactions.
993 let mut executable = Vec::new();
994 let mut remaining = Vec::new();
995 for transfer_addr in &state.queue {
996 let transfer_info = match remaining_accounts.get(transfer_addr) {
997 Some(transfer) => transfer,
998 None => {
999 remaining.push(*transfer_addr);
1000 continue;
1001 }
1002 };
1003 let mut ref_data = transfer_info.try_borrow_mut_data()?;
1004 let mut transfer_data: &[u8] = ref_data.deref_mut();
1005 let tx = Transfer::try_deserialize(&mut transfer_data)?;
1006 let to = match remaining_accounts.get(&tx.recipient) {
1007 None => return Err(Error::MissingRecipientAccountInfo.into()),
1008 Some(recipient) => recipient,
1009 };
1010 executable.push((transfer_info, to, tx.lamports));
1011 }
1012
1013 // There is no executable account info. Just returns the success.
1014 //
1015 // This is a case that the approver approved the multisig but didn't
1016 // provide the account info.
1017 if executable.is_empty() {
1018 return Ok(());
1019 }
1020
1021 // Executes the queued transfers.
1022 let fund = fund.to_account_info();
1023 for (transfer, to, lamports) in executable {
1024 // Fund to the recipient and closes the transfer account.
1025 State::transfer_fund(state, &fund, to, lamports, fund_bump)?;
1026 let lamports = transfer.lamports();
1027 State::transfer_fund(state, transfer, &fund, lamports, fund_bump)?;
1028 }
1029
1030 // Update the queue.
1031 state.queue = remaining;
1032
1033 // Reset the signed status once the queue is empty.
1034 if state.is_queue_empty() {
1035 state.signed.iter_mut().for_each(|signed| *signed = false);
1036 }
1037
1038 Ok(())
1039 }
1040
1041 /// Closes the multisig account.
1042 ///
1043 /// It cleans up all the remaining accounts and return those rents
1044 /// back to the funder, original creator of the multisig account.
1045 ///
1046 /// # Examples
1047 ///
1048 /// Here is how you close the multisig account on devnet:
1049 ///
1050 /// ```no_run
1051 /// use std::rc::Rc;
1052 ///
1053 /// use solana_sdk::commitment_config::CommitmentConfig;
1054 /// use solana_sdk::instruction::AccountMeta;
1055 /// use solana_sdk::pubkey::Pubkey;
1056 /// use solana_sdk::signature::read_keypair_file;
1057 /// use solana_sdk::signer::Signer;
1058 ///
1059 /// use anchor_client::{Client, Cluster};
1060 ///
1061 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1062 /// let url = Cluster::Devnet;
1063 /// let funder = Rc::new(read_keypair_file(
1064 /// shellexpand::tilde("~/.config/solana/id.json").as_ref(),
1065 /// )?);
1066 /// let opts = CommitmentConfig::processed();
1067 /// let pid = multisig_lite::id();
1068 /// let program = Client::new_with_options(url, funder.clone(), opts).program(pid);
1069 ///
1070 /// // Gets the PDAs.
1071 /// let (state_pda, state_bump) =
1072 /// Pubkey::find_program_address(&[b"state", funder.pubkey().as_ref()], &pid);
1073 /// let (fund_pda, fund_bump) = Pubkey::find_program_address(&[b"fund", state_pda.as_ref()], &pid);
1074 ///
1075 /// // Gets the remaining transfers to collects the rents.
1076 /// let state: multisig_lite::State = program.account(state_pda)?;
1077 /// let remaining_accounts: Vec<_> = state
1078 /// .queue
1079 /// .into_iter()
1080 /// .map(|pubkey| AccountMeta {
1081 /// pubkey,
1082 /// is_signer: false,
1083 /// is_writable: true,
1084 /// })
1085 /// .collect();
1086 ///
1087 /// // close the multisig account.
1088 /// let sig = program
1089 /// .request()
1090 /// .accounts(multisig_lite::accounts::Close {
1091 /// funder: funder.pubkey(),
1092 /// state: state_pda,
1093 /// fund: fund_pda,
1094 /// })
1095 /// .args(multisig_lite::instruction::Close {
1096 /// _state_bump: state_bump,
1097 /// fund_bump,
1098 /// })
1099 /// .signer(funder.as_ref())
1100 /// .send()?;
1101 ///
1102 /// println!("{sig}");
1103 /// # Ok(())
1104 /// # }
1105 /// ```
1106 #[allow(clippy::result_large_err)]
1107 pub fn close(ctx: Context<Close>, _state_bump: u8, fund_bump: u8) -> Result<()> {
1108 let funder = &mut ctx.accounts.funder;
1109 let state = &mut ctx.accounts.state;
1110 let fund = &mut ctx.accounts.fund;
1111 let remaining_accounts: HashMap<_, _> = ctx
1112 .remaining_accounts
1113 .iter()
1114 .map(|account| (account.key, account))
1115 .collect();
1116
1117 // Validate the multisig fund account.
1118 State::validate_fund(state, fund, fund_bump)?;
1119
1120 // Closes the transfer accounts by transfering the
1121 // rent fee back to the fund account.
1122 let to = fund.to_account_info();
1123 for transfer_addr in &state.queue {
1124 let from = match remaining_accounts.get(transfer_addr) {
1125 Some(transfer) => transfer,
1126 None => continue,
1127 };
1128 let lamports = from.lamports();
1129 State::transfer_fund(state, from, &to, lamports, fund_bump)?;
1130 }
1131
1132 // Closes the multisig fund account by transfering all the lamports
1133 // back to the funder.
1134 let from = fund.to_account_info();
1135 let to = funder.to_account_info();
1136 let lamports = fund.lamports();
1137 State::transfer_fund(state, &from, &to, lamports, fund_bump)?;
1138
1139 Ok(())
1140 }
1141}