Skip to main content

squads_multisig_program/state/
proposal.rs

1#![allow(deprecated)]
2use anchor_lang::prelude::*;
3
4use crate::errors::*;
5use crate::id;
6
7use anchor_lang::system_program;
8
9/// Stores the data required for tracking the status of a multisig proposal.
10/// Each `Proposal` has a 1:1 association with a transaction account, e.g. a `VaultTransaction` or a `ConfigTransaction`;
11/// the latter can be executed only after the `Proposal` has been approved and its time lock is released.
12#[account]
13pub struct Proposal {
14    /// The multisig this belongs to.
15    pub multisig: Pubkey,
16    /// Index of the multisig transaction this proposal is associated with.
17    pub transaction_index: u64,
18    /// The status of the transaction.
19    pub status: ProposalStatus,
20    /// PDA bump.
21    pub bump: u8,
22    /// Keys that have approved/signed.
23    pub approved: Vec<Pubkey>,
24    /// Keys that have rejected.
25    pub rejected: Vec<Pubkey>,
26    /// Keys that have cancelled (Approved only).
27    pub cancelled: Vec<Pubkey>,
28}
29
30impl Proposal {
31    pub fn size(members_len: usize) -> usize {
32        8 +   // anchor account discriminator
33        32 +  // multisig
34        8 +   // index
35        1 +   // status enum variant
36        8 +   // status enum wrapped timestamp (i64)
37        1 +   // bump
38        (4 + (members_len * 32)) + // approved vec
39        (4 + (members_len * 32)) + // rejected vec
40        (4 + (members_len * 32)) // cancelled vec
41    }
42
43    /// Register an approval vote.
44    pub fn approve(&mut self, member: Pubkey, threshold: usize) -> Result<()> {
45        // If `member` has previously voted to reject, remove that vote.
46        if let Some(vote_index) = self.has_voted_reject(member.key()) {
47            self.remove_rejection_vote(vote_index);
48        }
49
50        // Insert the vote of approval.
51        match self.approved.binary_search(&member) {
52            Ok(_) => return err!(MultisigError::AlreadyApproved),
53            Err(pos) => self.approved.insert(pos, member),
54        };
55
56        // If current number of approvals reaches threshold, mark the transaction as `Approved`.
57        if self.approved.len() >= threshold {
58            self.status = ProposalStatus::Approved {
59                timestamp: Clock::get()?.unix_timestamp,
60            };
61        }
62
63        Ok(())
64    }
65
66    /// Register a rejection vote.
67    pub fn reject(&mut self, member: Pubkey, cutoff: usize) -> Result<()> {
68        // If `member` has previously voted to approve, remove that vote.
69        if let Some(vote_index) = self.has_voted_approve(member.key()) {
70            self.remove_approval_vote(vote_index);
71        }
72
73        // Insert the vote of rejection.
74        match self.rejected.binary_search(&member) {
75            Ok(_) => return err!(MultisigError::AlreadyRejected),
76            Err(pos) => self.rejected.insert(pos, member),
77        };
78
79        // If current number of rejections reaches cutoff, mark the transaction as `Rejected`.
80        if self.rejected.len() >= cutoff {
81            self.status = ProposalStatus::Rejected {
82                timestamp: Clock::get()?.unix_timestamp,
83            };
84        }
85
86        Ok(())
87    }
88
89    /// Registers a cancellation vote.
90    pub fn cancel(&mut self, member: Pubkey, threshold: usize) -> Result<()> {
91        // Insert the vote of cancellation.
92        match self.cancelled.binary_search(&member) {
93            Ok(_) => return err!(MultisigError::AlreadyCancelled),
94            Err(pos) => self.cancelled.insert(pos, member),
95        };
96
97        // If current number of cancellations reaches threshold, mark the transaction as `Cancelled`.
98        if self.cancelled.len() >= threshold {
99            self.status = ProposalStatus::Cancelled {
100                timestamp: Clock::get()?.unix_timestamp,
101            };
102        }
103
104        Ok(())
105    }
106
107    /// Check if the member approved the transaction.
108    /// Returns `Some(index)` if `member` has approved the transaction, with `index` into the `approved` vec.
109    fn has_voted_approve(&self, member: Pubkey) -> Option<usize> {
110        self.approved.binary_search(&member).ok()
111    }
112
113    /// Check if the member rejected the transaction.
114    /// Returns `Some(index)` if `member` has rejected the transaction, with `index` into the `rejected` vec.
115    fn has_voted_reject(&self, member: Pubkey) -> Option<usize> {
116        self.rejected.binary_search(&member).ok()
117    }
118
119    /// Delete the vote of rejection at the `index`.
120    fn remove_rejection_vote(&mut self, index: usize) {
121        self.rejected.remove(index);
122    }
123
124    /// Delete the vote of approval at the `index`.
125    fn remove_approval_vote(&mut self, index: usize) {
126        self.approved.remove(index);
127    }
128
129    /// Check if the proposal account space needs to be reallocated to accommodate `cancelled` vec.
130    /// Proposal size is crated at creation, and thus may not accomodate enough space for all members to cancel if more are added or changed
131    /// Returns `true` if the account was reallocated.
132    pub fn realloc_if_needed<'a>(
133        proposal: AccountInfo<'a>,
134        members_length: usize,
135        rent_payer: Option<AccountInfo<'a>>,
136        system_program: Option<AccountInfo<'a>>,
137    ) -> Result<bool> {
138        // Sanity checks
139        require_keys_eq!(*proposal.owner, id(), MultisigError::IllegalAccountOwner);
140
141        let current_account_size = proposal.data.borrow().len();
142        let account_size_to_fit_members = Proposal::size(members_length);
143
144        // Check if we need to reallocate space.
145        if current_account_size >= account_size_to_fit_members {
146            return Ok(false);
147        }
148
149        // Reallocate more space.
150        AccountInfo::realloc(&proposal, account_size_to_fit_members, false)?;
151
152        // If more lamports are needed, transfer them to the account.
153        let rent_exempt_lamports = Rent::get()
154            .unwrap()
155            .minimum_balance(account_size_to_fit_members)
156            .max(1);
157        let top_up_lamports =
158            rent_exempt_lamports.saturating_sub(proposal.to_account_info().lamports());
159
160        if top_up_lamports > 0 {
161            let system_program = system_program.ok_or(MultisigError::MissingAccount)?;
162            require_keys_eq!(
163                *system_program.key,
164                system_program::ID,
165                MultisigError::InvalidAccount
166            );
167
168            let rent_payer = rent_payer.ok_or(MultisigError::MissingAccount)?;
169
170            system_program::transfer(
171                CpiContext::new(
172                    system_program,
173                    system_program::Transfer {
174                        from: rent_payer,
175                        to: proposal,
176                    },
177                ),
178                top_up_lamports,
179            )?;
180        }
181
182        Ok(true)
183    }
184}
185
186/// The status of a proposal.
187/// Each variant wraps a timestamp of when the status was set.
188#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug)]
189#[non_exhaustive]
190pub enum ProposalStatus {
191    /// Proposal is in the draft mode and can be voted on.
192    Draft { timestamp: i64 },
193    /// Proposal is live and ready for voting.
194    Active { timestamp: i64 },
195    /// Proposal has been rejected.
196    Rejected { timestamp: i64 },
197    /// Proposal has been approved and is pending execution.
198    Approved { timestamp: i64 },
199    /// Proposal is being executed. This is a transient state that always transitions to `Executed` in the span of a single transaction.
200    #[deprecated(
201        note = "This status used to be used to prevent reentrancy attacks. It is no longer needed."
202    )]
203    Executing,
204    /// Proposal has been executed.
205    Executed { timestamp: i64 },
206    /// Proposal has been cancelled.
207    Cancelled { timestamp: i64 },
208}