Skip to main content

squads_multisig_program/instructions/
proposal_vote.rs

1use anchor_lang::prelude::*;
2
3use crate::errors::*;
4use crate::state::*;
5
6#[derive(AnchorSerialize, AnchorDeserialize)]
7pub struct ProposalVoteArgs {
8    pub memo: Option<String>,
9}
10
11#[derive(Accounts)]
12pub struct ProposalVote<'info> {
13    #[account(
14        seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()],
15        bump = multisig.bump,
16    )]
17    pub multisig: Account<'info, Multisig>,
18
19    #[account(mut)]
20    pub member: Signer<'info>,
21
22    #[account(
23        mut,
24        seeds = [
25            SEED_PREFIX,
26            multisig.key().as_ref(),
27            SEED_TRANSACTION,
28            &proposal.transaction_index.to_le_bytes(),
29            SEED_PROPOSAL,
30        ],
31        bump = proposal.bump,
32    )]
33    pub proposal: Account<'info, Proposal>,
34}
35
36#[derive(Accounts)]
37pub struct ProposalCancelV2<'info> {
38    // The context needed for the ProposalVote instruction
39    pub proposal_vote: ProposalVote<'info>,
40
41    pub system_program: Program<'info, System>,
42}
43
44impl ProposalVote<'_> {
45    fn validate(&self, vote: Vote) -> Result<()> {
46        let Self {
47            multisig,
48            proposal,
49            member,
50            ..
51        } = self;
52
53        // member
54        require!(
55            multisig.is_member(member.key()).is_some(),
56            MultisigError::NotAMember
57        );
58        require!(
59            multisig.member_has_permission(member.key(), Permission::Vote),
60            MultisigError::Unauthorized
61        );
62
63        // proposal
64        match vote {
65            Vote::Approve | Vote::Reject => {
66                require!(
67                    matches!(proposal.status, ProposalStatus::Active { .. }),
68                    MultisigError::InvalidProposalStatus
69                );
70                // CANNOT approve or reject a stale proposal
71                require!(
72                    proposal.transaction_index > multisig.stale_transaction_index,
73                    MultisigError::StaleProposal
74                );
75            }
76            Vote::Cancel => {
77                require!(
78                    matches!(proposal.status, ProposalStatus::Approved { .. }),
79                    MultisigError::InvalidProposalStatus
80                );
81                // CAN cancel a stale proposal.
82            }
83        }
84
85        Ok(())
86    }
87
88    /// Approve a multisig proposal on behalf of the `member`.
89    /// The proposal must be `Active`.
90    #[access_control(ctx.accounts.validate(Vote::Approve))]
91    pub fn proposal_approve(ctx: Context<Self>, _args: ProposalVoteArgs) -> Result<()> {
92        let multisig = &mut ctx.accounts.multisig;
93        let proposal = &mut ctx.accounts.proposal;
94        let member = &mut ctx.accounts.member;
95
96        proposal.approve(member.key(), usize::from(multisig.threshold))?;
97
98        Ok(())
99    }
100
101    /// Reject a multisig proposal on behalf of the `member`.
102    /// The proposal must be `Active`.
103    #[access_control(ctx.accounts.validate(Vote::Reject))]
104    pub fn proposal_reject(ctx: Context<Self>, _args: ProposalVoteArgs) -> Result<()> {
105        let multisig = &mut ctx.accounts.multisig;
106        let proposal = &mut ctx.accounts.proposal;
107        let member = &mut ctx.accounts.member;
108
109        let cutoff = Multisig::cutoff(multisig);
110
111        proposal.reject(member.key(), cutoff)?;
112
113        Ok(())
114    }
115
116    /// Cancel a multisig proposal on behalf of the `member`.
117    /// The proposal must be `Approved`.
118    #[access_control(ctx.accounts.validate(Vote::Cancel))]
119    pub fn proposal_cancel(ctx: Context<Self>, _args: ProposalVoteArgs) -> Result<()> {
120        let multisig = &mut ctx.accounts.multisig;
121        let proposal = &mut ctx.accounts.proposal;
122        let member = &mut ctx.accounts.member;
123
124        proposal
125            .cancelled
126            .retain(|k| multisig.is_member(*k).is_some());
127
128        proposal.cancel(member.key(), usize::from(multisig.threshold))?;
129
130        Ok(())
131    }
132}
133
134impl<'info> ProposalCancelV2<'info> {
135
136    /// Cancel a multisig proposal on behalf of the `member`.
137    /// The proposal must be `Approved`.
138    pub fn proposal_cancel_v2(ctx: Context<'_, '_, 'info, 'info, Self>, _args: ProposalVoteArgs) -> Result<()> {
139        // Readonly accounts
140        let multisig = &ctx.accounts.proposal_vote.multisig.clone();
141
142        // Account infos necessary for reallocation
143        let proposal_account_info = &ctx.accounts.proposal_vote.proposal.to_account_info();
144        let member_account_info = &ctx.accounts.proposal_vote.member.to_account_info();
145        let system_program_account_info = &ctx.accounts.system_program.to_account_info();
146
147        // Create context for cancel instruction
148        let cancel_context = Context::new(ctx.program_id, &mut ctx.accounts.proposal_vote, ctx.remaining_accounts, ctx.bumps.proposal_vote);
149
150        // Call cancel instruction
151        ProposalVote::proposal_cancel(cancel_context, _args)?;
152
153        // Reallocate the proposal size if needed
154        Proposal::realloc_if_needed(
155            proposal_account_info.clone(),
156            multisig.members.len(),
157            Some(member_account_info.clone()),
158            Some(system_program_account_info.clone()),
159        )?;
160        Ok(())
161    }
162}
163
164pub enum Vote {
165    Approve,
166    Reject,
167    Cancel,
168}